├── .gitignore ├── README.md ├── __init__.py ├── engine_app_api.py ├── engine_communicator.py ├── engine_field_api.py ├── engine_generic_object_api.py ├── engine_global_api.py ├── engine_helper.py ├── pyqlikengine.py ├── structs.py └── test ├── __init__.py ├── test_app_api.py ├── test_data └── ctrl00_script.qvs ├── test_field_api.py ├── test_global_api.py ├── test_labs.py └── test_pyqlikengine.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # PyCharm project folder 92 | .idea/ 93 | 94 | # Merge stuff 95 | .swp 96 | 97 | # Notes file 98 | notes.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyqlikengine 2 | 3 | [This repository is no longer maintained by me since I have moved on to new challenges outside Qlik] 4 | 5 | Qlik Engine API extended for Python 6 | This is an attempt to implement a Qlik Engine API extension for Python. I started working on this May 7th 2017 and it is at a very experimental level. This is very much a hobby project that I will to work on it during my spare time, whenever I have enough inspiration to do so. 7 | 8 | It uses Python 3.6, the Python json encoder: https://docs.python.org/2/library/json.html and the websocket client: 9 | https://pypi.python.org/pypi/websocket-client 10 | 11 | 12 | 13 | /Niklas 14 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qliknln/pyqlikengine/720c2558cd88c0c6eab70a691b4261c70f5194d5/__init__.py -------------------------------------------------------------------------------- /engine_app_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class EngineAppApi: 5 | 6 | def __init__(self, socket): 7 | self.engine_socket = socket 8 | 9 | def get_script(self, doc_handle): 10 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetScript", "params": []}) 11 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 12 | try: 13 | return response['result'] 14 | except KeyError: 15 | return response['error'] 16 | 17 | def set_script(self, doc_handle, script): 18 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "SetScript", "params": [script]}) 19 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 20 | try: 21 | return response['result'] 22 | except KeyError: 23 | return response['error'] 24 | 25 | def do_reload(self, doc_handle, param_list=[]): 26 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "DoReload", "params": param_list}) 27 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 28 | try: 29 | return response['result'] 30 | except KeyError: 31 | return response['error'] 32 | 33 | def do_reload_ex(self, doc_handle, param_list=[]): 34 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "DoReloadEx", "params": param_list}) 35 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 36 | try: 37 | return response['result'] 38 | except KeyError: 39 | return response['error'] 40 | 41 | def get_app_layout(self, doc_handle): 42 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetAppLayout", "params": []}) 43 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 44 | try: 45 | return response['result'] 46 | except KeyError: 47 | return response['error'] 48 | 49 | def get_object(self, doc_handle, param_list=[]): 50 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetObject", "params": param_list}) 51 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 52 | try: 53 | return response['result'] 54 | except KeyError: 55 | return response['error'] 56 | 57 | def get_field(self, doc_handle, field_name, state_name=""): 58 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetField", "params": 59 | {"qFieldName": field_name, "qStateName": state_name}}) 60 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 61 | try: 62 | return response['result'] 63 | except KeyError: 64 | return response['error'] 65 | 66 | def create_object(self, doc_handle, q_id="LB01", q_type = "ListObject", struct_name="qListObjectDef", 67 | ob_struct={}): 68 | msg=json.dumps({"jsonrpc": "2.0", "id": 0, "method": "CreateObject", "handle": doc_handle, 69 | "params": [{"qInfo": {"qId": q_id, "qType": q_type},struct_name: ob_struct}]}) 70 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 71 | try: 72 | return response['result'] 73 | except KeyError: 74 | return response['error'] 75 | 76 | # AddAlternateState method: Create an alternate state in app 77 | # You can create multiple states within a Qlik Sense app and apply these states to specific objects within the app. 78 | # Objects in a given state are not affected by user selections in the other states. 79 | # Call GetAppLayout() afterwards to get the latest states 80 | def add_alternate_state(self, doc_handle, state_name): 81 | msg = json.dumps( 82 | {"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "AddAlternateState", "params": [state_name]}) 83 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 84 | try: 85 | return response['result'] 86 | except KeyError: 87 | return response['error'] 88 | 89 | # AddFieldFromExpression method: Adds a field on the fly. !! The expression of a field on the fly is persisted but 90 | # not its values. !! 91 | def add_field_from_expression(self, doc_handle, field_name, expr_value): 92 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "AddFieldFromExpression", 93 | "params": [field_name, expr_value]}) 94 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 95 | try: 96 | return response['result'] 97 | except KeyError: 98 | return response['error'] 99 | 100 | # CheckExpression method: Checks whether an expression is valid or not 101 | # qErrorMsg is empty if it's valid 102 | def check_expression(self, doc_handle, expr_value): 103 | msg = json.dumps( 104 | {"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "CheckExpression", "params": [expr_value]}) 105 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 106 | try: 107 | return response['result'] 108 | except KeyError: 109 | return response['error'] 110 | 111 | # CheckScriptSyntax method: Checks whether a load script is valid or not 112 | # Used AFTER doing SetScript method 113 | # errors are displayed in an array discussing positions of characters in script where failing 114 | def check_script(self, doc_handle, expr_value): 115 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "CheckScriptSyntax", 116 | "params": [expr_value]}) 117 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 118 | try: 119 | return response['result'] 120 | except KeyError: 121 | return response['error'] 122 | 123 | def clear_all(self, doc_handle, locked_also=False, alt_state=""): 124 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "ClearAll", 125 | "params": [locked_also, alt_state]}) 126 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 127 | try: 128 | return response['result'] 129 | except KeyError: 130 | return response['error'] 131 | 132 | # CreateConnection method: Creates a connection. A connection indicates from which data source, the data should 133 | # be taken. The connection can be: an ODBC connection, OLEDB connection, a custom connection, a folder connection 134 | # (lib connection), an internet connection, Single Sign-On 135 | def create_connection(self, doc_handle, connect_name, connect_string, connect_type, user_name, password, 136 | mod_date="", meta="", sso_passthrough="LOG_ON_SERVICE_USER"): 137 | msg = json.dumps( 138 | {"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "CreateConnection", "params": [{ 139 | "qName": connect_name, 140 | "qMeta": meta, 141 | "qConnectionString": connect_string, 142 | "qType": connect_type 143 | }]}) 144 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 145 | try: 146 | return response['result'] 147 | except KeyError: 148 | return response['error'] 149 | 150 | # CreateDimension method: Creates a master dimension. 151 | # A Master Dimension is stored in the library of an app and can be used in many objects. Several generic objects 152 | # can contain the same dimension. 153 | # Parameters: 154 | # qProp (MANDATORY: send dim_id, dim_title, dim_grouping, dim_field, dim_label, meta_def (optional) 155 | def create_master_dim(self, doc_handle, dim_id, dim_title, dim_grouping="N", dim_field='', dim_label='', 156 | meta_def=""): 157 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "CreateDimension", "params": [{ 158 | "qInfo": { 159 | "qId": dim_id, 160 | "qType": "Dimension" 161 | }, 162 | "qDim": { 163 | "title": dim_title, 164 | "qGrouping": dim_grouping, 165 | "qFieldDefs": [ 166 | dim_field 167 | ], 168 | "qFieldLabels": [ 169 | dim_label 170 | ] 171 | }, 172 | "qMetaDef": { 173 | "title": meta_def 174 | } 175 | }]}) 176 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 177 | try: 178 | return response['result'] 179 | except KeyError: 180 | return response['error'] 181 | 182 | # DestroyDimension method: Removes a dimension 183 | def destroy_dim(self, doc_handle, dim_id): 184 | msg = json.dumps( 185 | {"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "DestroyDimension", "params": [{dim_id}]}) 186 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 187 | try: 188 | return response['result'] 189 | except KeyError: 190 | return response['error'] 191 | 192 | # DestroyMeasure method: Removes a measure 193 | def destroy_measure(self, doc_handle, measure_id): 194 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "DestroyDimension", 195 | "params": [{measure_id}]}) 196 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 197 | try: 198 | return response['result'] 199 | except KeyError: 200 | return response['error'] 201 | 202 | # DestroyObject method: Removes an app object. The children of the object (if any) are removed as well. 203 | def destroy_object(self, doc_handle, object_id): 204 | msg = json.dumps( 205 | {"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "DestroyObject", "params": [{object_id}]}) 206 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 207 | try: 208 | return response['result'] 209 | except KeyError: 210 | return response['error'] 211 | 212 | # DestroySessionObject method: Removes a session object. The children of the object (if any) are removed as well. 213 | def destroy_session_object(self, doc_handle, object_id): 214 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "DestroySessionObject", 215 | "params": [{object_id}]}) 216 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 217 | try: 218 | return response['result'] 219 | except KeyError: 220 | return response['error'] 221 | 222 | # DestroySessionVariable method: Removes an transient variable. 223 | def destroy_session_variable(self, doc_handle, var_id): 224 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "DestroySessionVariable", 225 | "params": [{var_id}]}) 226 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 227 | try: 228 | return response['result'] 229 | except KeyError: 230 | return response['error'] 231 | 232 | # DestroyVariableById method: Removes a varable.. 233 | # Script-defined variables cannot be removed using the DestroyVariableById method or the 234 | # DestroyVariableByName method. 235 | def destroy_variable_by_id(self, doc_handle, var_name): 236 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "DestroyVariableById", 237 | "params": [{var_name}]}) 238 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 239 | try: 240 | return response['result'] 241 | except KeyError: 242 | return response['error'] 243 | 244 | # CreateMeasure method: Creates a master dimension. 245 | # A Master Dimension is stored in the library of an app and can be used in many objects. Several generic objects 246 | # can contain the same dimension. 247 | # Parameters: 248 | # qProp (MANDATORY: send dim_id, dim_title, dim_grouping, dim_field, dim_label, meta_def (optional) 249 | def create_master_measure(self, doc_handle, measure_id, measure_title, measure_expr, meta_def=""): 250 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "CreateMeasure", "params": [{ 251 | "qInfo": { 252 | "qId": measure_id, 253 | "qType": "Measure" 254 | }, 255 | "qMeasure": { 256 | "qLabel": measure_title, 257 | "qDef": measure_expr 258 | }, 259 | "qMetaDef": { 260 | "title": measure_title 261 | } 262 | }]}) 263 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 264 | try: 265 | return response['result'] 266 | except KeyError: 267 | return response['error'] 268 | 269 | # CreateObject method: Creates a generic object at app level. It is possible to create a generic object that is 270 | # linked to another object. A linked object is an object that points to a linking object. The linking object is 271 | # defined in the properties of the linked object (in qExtendsId). The linked object has the same properties as the 272 | # linking object. 273 | # TODO: Come back to this - Very important that it is well understood how we want to create objects / datasets from 274 | # python in app 275 | # Convert hypercube to dict or some other data set 276 | 277 | 278 | # CreateSession Object method: Creates a generic object at app level. It is possible to create a generic object that is linked to another object. A linked object is an object that points to a linking object. The linking object is defined in the properties of the linked object (in qExtendsId). The linked object has the same properties as the linking object. 279 | # TODO: Come back to this - Very important that it is well understood how we want to create objects / datasets from 280 | # python in app 281 | # Convert hypercube to dict or some other data set 282 | 283 | # CreateSessionVariable method: 284 | # A variable in Qlik Sense is a named entity, containing a data value. This value can be static or be the result of a calculation. A variable acquires its value at the same time that the variable is created or after when updating the properties of the variable. Variables can be used in bookmarks and can contain numeric or alphanumeric data. Any change made to the variable is applied everywhere the variable is used. 285 | # When a variable is used in an expression, it is substituted by its value or the variable's definition. 286 | #### Example: The variable x contains the text string Sum(Sales). In a chart, you define the expression $(x)/12. The effect is exactly the same as having the chart expression Sum(Sales)/12. However, if you change the value of the variable x to Sum(Budget), the data in the chart are immediately recalculated with the expression interpreted as Sum(Budget)/12. 287 | def create_session_variable(self, doc_handle, var_id="", var_name="", var_comment="", var_def=""): 288 | msg = json.dumps( 289 | {"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "CreateSessionVariable", "params": [{ 290 | "qInfo": { 291 | "qId": var_id, 292 | "qType": "Variable" 293 | }, 294 | "qName": var_name, 295 | "qComment": var_comment, 296 | "qDefinition": var_def 297 | }]}) 298 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 299 | try: 300 | return response['result'] 301 | except KeyError: 302 | return response['error'] 303 | 304 | # CreateVariable method: 305 | # A variable in Qlik Sense is a named entity, containing a data value. This value can be static or be the result of a calculation. A variable acquires its value at the same time that the variable is created or after when updating the properties of the variable. Variables can be used in bookmarks and can contain numeric or alphanumeric data. Any change made to the variable is applied everywhere the variable is used. 306 | # When a variable is used in an expression, it is substituted by its value or the variable's definition. 307 | #### Example: The variable x contains the text string Sum(Sales). In a chart, you define the expression $(x)/12. The effect is exactly the same as having the chart expression Sum(Sales)/12. However, if you change the value of the variable x to Sum(Budget), the data in the chart are immediately recalculated with the expression interpreted as Sum(Budget)/12. 308 | def create_variable(self, doc_handle, var_id="", var_name="", var_comment="", var_def=""): 309 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "CreateVariable", "params": [{ 310 | "qInfo": { 311 | "qId": var_id, 312 | "qType": "Variable" 313 | }, 314 | "qName": var_name, 315 | "qComment": var_comment, 316 | "qDefinition": var_def 317 | }]}) 318 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 319 | try: 320 | return response['result'] 321 | except KeyError: 322 | return response['error'] 323 | 324 | # DoReload method: Reloads the script that is set in an app. 325 | # Parameters: 326 | # qMode (optional): Error handling mode (Integer).. 0: for default mode, 1: for ABEND; the reload of the script ends if an error occurs., 2: for ignore; the reload of the script continues even if an error is detected in the script. 327 | # qPartial (optional): Set to true for partial reload, The default value is false. 328 | # qDebug (optional): Set to true if debug breakpoints are to be honored. The execution of the script will be in debug mode. The default value is false. 329 | 330 | def do_reload(self, doc_handle, reload_mode=0, partial_mode=False, debug_mode=False): 331 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "DoReload", 332 | "params": [reload_mode, partial_mode, debug_mode]}) 333 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 334 | try: 335 | return response['result'] 336 | except KeyError: 337 | return response['error'] 338 | 339 | # DoSave method: Saves an app - All objects and data in the data model are saved. 340 | # Desktop only - server auto saves 341 | def do_save(self, doc_handle): 342 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "DoSave", "params": []}) 343 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 344 | try: 345 | return response['result'] 346 | except KeyError: 347 | return response['error'] 348 | 349 | # Evaluate method: Evaluates an expression as a string. (Actually uses EvaluateEx, which is better for giving the data type back to python) 350 | # Parameters: qExpression 351 | def expr_eval(self, doc_handle, expr): 352 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "EvaluateEx", 353 | "params": {"qExpression": expr}}) 354 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 355 | try: 356 | return response['result'] 357 | except KeyError: 358 | return response['error'] 359 | 360 | # GetAllInfos method: Get the identifier and the type of any generic object in an app by using the GetAllInfos method. 361 | def get_all_infos(self, doc_handle): 362 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetAllInfos", "params": []}) 363 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 364 | try: 365 | return response['result'] 366 | except KeyError: 367 | return response['error'] 368 | 369 | # GetAppProperties method: Gets the properties of an app. 370 | def get_app_properties(self, doc_handle): 371 | msg = json.dumps( 372 | {"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetAppProperties", "params": []}) 373 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 374 | try: 375 | return response['result'] 376 | except KeyError: 377 | return response['error'] 378 | 379 | # GetConnection method: Retrieves a connection and returns: The creation time of the connection, The identifier of 380 | # the connection, The type of the connection, The name of the connection, The connection string 381 | def get_connection(self, doc_handle, connection_id): 382 | msg = json.dumps( 383 | {"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetConnection", "params": [connection_id]}) 384 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 385 | try: 386 | return response['result'] 387 | except KeyError: 388 | return response['error'] 389 | 390 | # GetConnections method: Lists the connections in an app 391 | def get_connections(self, doc_handle): 392 | msg = json.dumps( 393 | {"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetConnections", "params": []}) 394 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 395 | try: 396 | return response['result'] 397 | except KeyError: 398 | return response['error'] 399 | 400 | # GetDatabaseInfo: Get information about an ODBC, OLEDB or CUSTOM connection 401 | def get_db_info(self, doc_handle, connection_id): 402 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetDatabaseInfo", 403 | "params": [connection_id]}) 404 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 405 | try: 406 | return response['result'] 407 | except KeyError: 408 | return response['error'] 409 | 410 | # GetDatabaseOwners: List the owners of a database for a ODBC, OLEDB or CUSTOM connection 411 | def get_db_owners(self, doc_handle, connection_id): 412 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetDatabaseOwners", 413 | "params": [connection_id]}) 414 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 415 | try: 416 | return response['result'] 417 | except KeyError: 418 | return response['error'] 419 | 420 | # GetDatabases: List the databases of a ODBC, OLEDB or CUSTOM connection 421 | def get_databases(self, doc_handle, connection_id): 422 | msg = json.dumps( 423 | {"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetDatabases", "params": [connection_id]}) 424 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 425 | try: 426 | return response['result'] 427 | except KeyError: 428 | return response['error'] 429 | 430 | # GetDatabaseTableFields: List the fields in a table for a ODBC, OLEDB or CUSTOM connection 431 | # Parameters taken are: connection_id (mandatory), db_name, db_owner, table_name (mandatory) 432 | def get_db_table_fields(self, doc_handle, connection_id, db_name="", db_owner="", table_name=""): 433 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetDatabaseTableFields", 434 | "params": [connection_id, db_name, db_owner, table_name]}) 435 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 436 | try: 437 | return response['result'] 438 | except KeyError: 439 | return response['error'] 440 | 441 | # GetDatabaseTablePreview: Preview the data in the fields in a table for a ODBC, OLEDB or CUSTOM connection 442 | # Parameters taken are: connection_id (mandatory), db_name, db_owner, table_name (mandatory) 443 | def get_db_table_preview(self, doc_handle, connection_id, db_name="", db_owner="", table_name=""): 444 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetDatabaseTablePreview", 445 | "params": [connection_id, db_name, db_owner, table_name]}) 446 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 447 | try: 448 | return response['result'] 449 | except KeyError: 450 | return response['error'] 451 | 452 | # GetDatabaseTables: List the tables in a database for a specific owner and for a ODBC, OLEDB or CUSTOM connection 453 | # Parameters taken are: connection_id (mandatory), db_name, db_owner 454 | def get_db_tables(self, doc_handle, connection_id, db_name="", db_owner=""): 455 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetDatabaseTables", 456 | "params": [connection_id, db_name, db_owner]}) 457 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 458 | try: 459 | return response['result'] 460 | except KeyError: 461 | return response['error'] 462 | 463 | # GetDimension: Get the handle of a dimension by using the GetDimension method. 464 | # Parameter: dimension id 465 | def get_dim_handle(self, doc_handle, dim_id): 466 | msg = json.dumps( 467 | {"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetDimension", "params": [dim_id]}) 468 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 469 | try: 470 | return response['result'] 471 | except KeyError: 472 | return response['error'] 473 | 474 | # GetEmptyScript: Creates a script that contains one section. This section contains Set statements that give 475 | # localized information from the regional settings of the computer. 476 | # Parameter: none 477 | def get_empty_script(self, doc_handle): 478 | msg = json.dumps( 479 | {"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetEmptyScript", "params": []}) 480 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 481 | try: 482 | return response['result'] 483 | except KeyError: 484 | return response['error'] 485 | 486 | # GetFieldDescription: Get the description of a field 487 | # Parameter: field name 488 | def get_field_descr(self, doc_handle, field_name): 489 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetFieldDescription", 490 | "params": {"qFieldName": field_name}}) 491 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 492 | try: 493 | return response['result'] 494 | except KeyError: 495 | return response['error'] 496 | 497 | # GetField method: Retrieves the handle of a field. 498 | # Parameter: field name 499 | def get_field_handle(self, doc_handle, field_name): 500 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetField", "params": 501 | [field_name]}) 502 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 503 | try: 504 | return response['result'] 505 | except KeyError: 506 | return response['error'] 507 | 508 | # GetFileTableFields method: Lists the fields of a table for a folder connection. 509 | # Parameters: 510 | # qConnectionId (MANDATORY): Identifier of the connection. 511 | # qRelativePath: Path of the connection file 512 | # qDataFormat: Type of the file 513 | # qTable (MOSTLY MANDATORY): Name of the table ***This parameter must be set for XLS, XLSX, HTML and XML files.*** 514 | def get_file_table_fields(self, doc_handle, connection_id, rel_path="", data_fmt="", table_name=""): 515 | msg = json.dumps( 516 | {"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetFileTableFields", "params": [ 517 | connection_id, rel_path, {"qType": data_fmt}, table_name]}) 518 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 519 | try: 520 | return response['result'] 521 | except KeyError: 522 | return response['error'] 523 | 524 | # GetFileTablePreview method: Preview the data in the fields of a table for a folder connection. 525 | # Parameters: 526 | # qConnectionId (MANDATORY): Identifier of the connection. 527 | # qRelativePath: Path of the connection file 528 | # qDataFormat: Type of the file 529 | # qTable (MOSTLY MANDATORY): Name of the table ***This parameter must be set for XLS, XLSX, HTML and XML files.*** 530 | def get_file_table_preview(self, doc_handle, connection_id, rel_path="", data_fmt="", table_name=""): 531 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetFileTablePreview", "params": [ 532 | connection_id, rel_path, {"qType": data_fmt}, table_name]}) 533 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 534 | try: 535 | return response['result'] 536 | except KeyError: 537 | return response['error'] 538 | 539 | # GetFileTablesEx method: List the tables and fields of a XML file or from a JSON file, for a folder connection 540 | # Parameters: 541 | # qConnectionId (MANDATORY): Identifier of the connection. 542 | # qRelativePath: Path of the connection file 543 | # qDataFormat: Type of the file (XML, JSON) 544 | # qTable (MOSTLY MANDATORY): Name of the table ***This parameter must be set for XLS, XLSX, HTML and XML files.*** 545 | def get_file_table_ex(self, doc_handle, connection_id, rel_path="", data_fmt=""): 546 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetFileTablesEx", "params": [ 547 | connection_id, rel_path, {"qType": data_fmt}]}) 548 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 549 | try: 550 | return response['result'] 551 | except KeyError: 552 | return response['error'] 553 | 554 | # GetFileTables method: Lists the tables for a folder connection. 555 | # Parameters: 556 | # qConnectionId (MANDATORY): Identifier of the connection. 557 | # qRelativePath: Path of the connection file 558 | # qDataFormat: Type of the file (XML, JSON) 559 | def get_file_tables(self, doc_handle, connection_id, rel_path="", data_fmt=""): 560 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetFileTables", "params": [ 561 | connection_id, rel_path, {"qType": data_fmt}]}) 562 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 563 | try: 564 | return response['result'] 565 | except KeyError: 566 | return response['error'] 567 | 568 | # GetFolderItemsForConnection method: List the items for a folder connection 569 | # Parameter: connection_id 570 | def get_folder_items_for_connection(self, doc_handle, connection_id): 571 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "GetFolderItemsForConnection", 572 | "params": [connection_id]}) 573 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 574 | try: 575 | return response['result'] 576 | except KeyError: 577 | return response['error'] 578 | 579 | def create_session_object(self, doc_handle, param): 580 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": doc_handle, "method": "CreateSessionObject", 581 | "params": [param]}) 582 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 583 | try: 584 | return response['result'] 585 | except KeyError: 586 | return response['error'] -------------------------------------------------------------------------------- /engine_communicator.py: -------------------------------------------------------------------------------- 1 | from websocket import create_connection 2 | import jwt 3 | import ssl 4 | 5 | 6 | class EngineCommunicator: 7 | 8 | def __init__(self, url): 9 | self.url = url 10 | self.ws = create_connection(self.url) 11 | self.session = self.ws.recv() # Holds session object. Required for Qlik Sense Sept. 2017 and later 12 | 13 | @staticmethod 14 | def send_call(self, call_msg): 15 | self.ws.send(call_msg) 16 | return self.ws.recv() 17 | 18 | @staticmethod 19 | def close_qvengine_connection(self): 20 | self.ws.close() 21 | 22 | class SecureEngineCommunicator(EngineCommunicator): 23 | 24 | def __init__(self, senseHost, proxyPrefix, userDirectory, userId, privateKeyPath, ignoreCertErrors=False): 25 | self.url = "wss://" + senseHost + "/" + proxyPrefix + "/app/engineData" 26 | sslOpts = {} 27 | if ignoreCertErrors: 28 | sslOpts = {"cert_reqs": ssl.CERT_NONE} 29 | 30 | privateKey = open(privateKeyPath).read() 31 | token = jwt.encode({'user': userId, 'directory': userDirectory}, privateKey, algorithm='RS256') 32 | 33 | self.ws = create_connection(self.url, sslopt=sslOpts, header=['Authorization: BEARER ' + str(token)]) 34 | self.session = self.ws.recv() -------------------------------------------------------------------------------- /engine_field_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class EngineFieldApi: 5 | 6 | def __init__(self, socket): 7 | self.engine_socket = socket 8 | 9 | def select_values(self, fld_handle, values=None): 10 | if values is None: 11 | values = [] 12 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": fld_handle, "method": "SelectValues", 13 | "params": [values, False, False]}) 14 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 15 | try: 16 | return response 17 | except KeyError: 18 | return response["error"] 19 | 20 | def select_excluded(self, fld_handle): 21 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": fld_handle, "method": "SelectExcluded", 22 | "params": []}) 23 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 24 | try: 25 | return response["result"] 26 | except KeyError: 27 | return response["error"] 28 | 29 | def select_possible(self, fld_handle): 30 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": fld_handle, "method": "SelectPossible", 31 | "params": []}) 32 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 33 | try: 34 | return response["result"] 35 | except KeyError: 36 | return response["error"] 37 | 38 | def clear(self, fld_handle): 39 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": fld_handle, "method": "SelectExcluded", 40 | "params": []}) 41 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 42 | try: 43 | return response["result"] 44 | except KeyError: 45 | return response["error"] 46 | 47 | def get_cardinal(self, fld_handle): 48 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": fld_handle, "method": "GetCardinal", 49 | "params": []}) 50 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 51 | try: 52 | return response["result"] 53 | except KeyError: 54 | return response["error"] -------------------------------------------------------------------------------- /engine_generic_object_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class EngineGenericObjectApi: 5 | 6 | def __init__(self, socket): 7 | self.engine_socket = socket 8 | 9 | def get_layout(self, handle): 10 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": handle, "method": "GetLayout", "params": []}) 11 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 12 | try: 13 | return response["result"] 14 | except KeyError: 15 | return response["error"] 16 | 17 | def get_hypercube_data(self, handle, path="/qHyperCubeDef", pages=[]): 18 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": handle, "method": "GetHyperCubeData", 19 | "params": [path,pages]}) 20 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 21 | try: 22 | return response["result"] 23 | except KeyError: 24 | return response["error"] 25 | 26 | def get_list_object_data(self, handle, path="/qListObjectDef", pages=[]): 27 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": handle, "method": "GetListObjectData", 28 | "params": [path, pages]}) 29 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 30 | try: 31 | return response["result"] 32 | except KeyError: 33 | return response["error"] 34 | -------------------------------------------------------------------------------- /engine_global_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class EngineGlobalApi: 5 | def __init__(self, socket): 6 | self.engine_socket = socket 7 | 8 | # returns an array of doc objects. The doc object contains doc name, size, file time etc 9 | def get_doc_list(self): 10 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "GetDocList", "params": []}) 11 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 12 | try: 13 | return response['result']['qDocList'] 14 | except KeyError: 15 | return response['error'] 16 | 17 | # returns the os name (always windowsNT). Obsolete? 18 | def get_os_name(self): 19 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "OSName", "params": []}) 20 | response =json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 21 | try: 22 | return response['result']['qReturn'] 23 | except KeyError: 24 | return response['error'] 25 | 26 | # returns the app id. If desktop is used the app id is the same as the full path to qvf 27 | # if it's running against Enterprise, app id will be a guid 28 | def create_app(self, app_name): 29 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "CreateApp", "params": [app_name]}) 30 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 31 | try: 32 | return response['result'] 33 | except KeyError: 34 | return response["error"] 35 | 36 | 37 | # DeleteApp Method Deletes an app from the Qlik Sense repository or from the file system. Qlik Sense Enterprise: 38 | # In addition to being removed from the repository, the app is removed from the directory as well: 39 | # \Qlik\Sense\Apps The default installation directory is ProgramData. Qlik Sense Desktop: 40 | # The app is deleted from the directory %userprofile%\Documents\Qlik\Sense\Apps. Parameters: qAppId.. Identifier 41 | # of the app to delete. In Qlik Sense Enterprise, the identifier of the app is a GUID in the Qlik Sense 42 | # repository. In Qlik Sense Desktop, the identifier of the app is the name of the app, as defined in the apps 43 | # folder %userprofile%\Documents\Qlik\Sense\Apps. This parameter is mandatory. 44 | def delete_app(self, app_name): 45 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "DeleteApp", "params": [app_name]}) 46 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 47 | try: 48 | return response['result'] 49 | except KeyError: 50 | return response["error"] 51 | 52 | # opens an app and returns an object with handle, generic id and type 53 | def open_doc(self, app_name, user_name='', password='', serial='', no_data=False): 54 | msg = json.dumps( 55 | {"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "OpenDoc", "params": [app_name, user_name, 56 | password, serial, 57 | no_data]}) 58 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 59 | try: 60 | return response['result'] 61 | except KeyError: 62 | return response["error"] 63 | 64 | # returns an object with handle, generic id and type for the active app 65 | def get_active_doc(self): 66 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "GetActiveDoc", "params": []}) 67 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 68 | try: 69 | return response['result'] 70 | except KeyError: 71 | return response["error"] 72 | 73 | @staticmethod 74 | def get_handle(obj): 75 | try: 76 | return obj["qHandle"] 77 | except ValueError: 78 | return "Bad handle value in " + obj 79 | 80 | # Abort All commands 81 | def abort_all(self): 82 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "AbortAll", "params": []}) 83 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 84 | try: 85 | return response['result'] 86 | except KeyError: 87 | return response["error"] 88 | 89 | # Abort Specific Request 90 | def abort_request(self, request_id): 91 | msg = json.dumps( 92 | {"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "AbortRequest", "params": {"qRequestId": request_id}}) 93 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 94 | try: 95 | return response['result'] # ['qReturn'] 96 | except KeyError: 97 | return response["error"] 98 | 99 | # Configure Reload - This is done before doing a reload qCancelOnScriptError: If set to true, the script 100 | # execution is halted on error. Otherwise, the engine continues the script execution. This parameter is relevant 101 | # only if the variable ErrorMode is set to 1. qUseErrorData: If set to true, any script execution error is 102 | # returned in qErrorData by the GetProgress method. qInteractOnError: If set to true, the script execution is 103 | # halted on error and the engine is waiting for an interaction to be performed. If the result from the 104 | # interaction is 1 (qDef.qResult is 1), the engine continues the script execution otherwise the execution is 105 | # halted. This parameter is relevant only if the variable ErrorMode is set to 1 and the script is run in debug 106 | # mode (qDebug is set to true when calling the DoReload method). 107 | def configure_reload(self, cancel_on_error=False, use_error_data=True, interact_on_error=False): 108 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "ConfigureReload", 109 | "params": {"qCancelOnScriptError": cancel_on_error, "qUseErrorData": use_error_data, 110 | "qInteractOnError": interact_on_error}}) 111 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 112 | try: 113 | return response['result'] 114 | except KeyError: 115 | return response["error"] 116 | 117 | # Copy app - This is done before doing a reload qTargetAppId (MANDATORY): Identifier (GUID) of the app 118 | # entity in the Qlik Sense repository. The app entity must have been previously created by the repository (via 119 | # the REST API). qSrcAppId (MANDATORY): Identifier (GUID) of the source app in the Qlik Sense repository. Array 120 | # of QRS identifiers. The list of all the objects in the app to be copied must be given. This list must contain 121 | # the GUIDs of all these objects. If the list of the QRS identifiers is empty, the CopyApp method copies all 122 | # objects to the target app. Script-defined variables are automatically copied when copying an app. To be able to 123 | # copy variables not created via script, the GUID of each variable must be provided in the list of QRS 124 | # identifiers. To get the QRS identifiers of the objects in an app, you can use the QRS API. The GET method (from 125 | # the QRS API) returns the identifiers of the objects in the app. The following example returns the QRS 126 | # identifiers of all the objects in a specified app: GET /qrs/app/9c3f8634-6191-4a34-a114-a39102058d13 Where 127 | # 9c3f8634-6191-4a34-a114-a39102058d13 is the identifier of the app. 128 | 129 | # BUG - Does not work in September 2017 release 130 | def copy_app(self, target_app_id, src_app_id, qIds=[""]): 131 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "CopyApp", 132 | "params": {"qTargetAppId": target_app_id, "qSrcAppId": src_app_id, "qIds": qIds}}) 133 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 134 | try: 135 | return response['result'] 136 | except KeyError: 137 | return response["error"] 138 | 139 | # Creates an empty session app. The following applies: The name of a session app cannot be chosen. The engine 140 | # automatically assigns a unique identifier to the session app. A session app is not persisted and cannot be 141 | # saved. Everything created during a session app is non-persisted; for example: objects, data connections. 142 | def create_session_app(self): 143 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "CreateSessionApp", "params": {}}) 144 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 145 | try: 146 | return response['result'] 147 | except KeyError: 148 | return response["error"] 149 | 150 | # Return the session App Id to use for subsequent calls 151 | # The identifier of the session app is composed of the prefix SessionApp_ and of a GUID. 152 | # ['qReturn'] 153 | 154 | # Create an empty session app from an Existing App The objects in the source app are copied into the session app 155 | # but contain no data. The script of the session app can be edited and reloaded. The name of a session app cannot 156 | # be chosen. The engine automatically assigns a unique identifier to the session app. A session app is not 157 | # persisted and cannot be saved. Everything created during a session app is non-persisted; for example: objects, 158 | # data connections. 159 | def create_session_app_from_app(self, src_app_id): 160 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "CreateSessionAppFromApp", 161 | "params": {"qSrcAppId": src_app_id}}) 162 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 163 | try: 164 | return response['result'] 165 | except KeyError: 166 | return response["error"] 167 | 168 | # ExportApp method: Exports an app from the Qlik Sense repository to the file system. !!! This operation is 169 | # possible only in Qlik Sense Enterprise. !!! Parameters: qTargetPath (MANDATORY) - Path and name of the target 170 | # app qSrcAppId (MANDATORY) - Identifier of the source app. The identifier is a GUID from the Qlik Sense 171 | # repository. qIds - Array of identifiers.. The list of all the objects in the app to be exported must be given. 172 | # This list must contain the GUIDs of all these objects. 173 | def export_app(self, target_path, src_app_id, qIds = [""]): 174 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "ExportApp", 175 | "params": {"qTargetPath": target_path, "qSrcAppId": src_app_id, "qIds": qIds}}) 176 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 177 | try: 178 | return response['result'] 179 | except KeyError: 180 | return response["error"] 181 | 182 | # ReplaceAppFromID method: Replaces an app with the objects from a source app. The list of objects in the app to 183 | # be replaced must be defined in qIds. !!! This operation is possible only in Qlik Sense Enterprise. !!! 184 | # Parameters: qTargetAppId (MANDATORY) - Identifier (GUID) of the target app. The target app is the app to be 185 | # replaced. qSrcAppId (MANDATORY) - Identifier of the source app. The identifier is a GUID from the Qlik Sense 186 | # repository. qIds - QRS identifiers (GUID) of the objects in the target app to be replaced. Only QRS-approved 187 | # GUIDs are applicable. An object that is QRS-approved, is for example an object that has been published (i.e not 188 | # private anymore). If an object is private, it should not be included in this list. If qIds is empty, 189 | # the engine automatically creates a list that contains all QRS-approved objects. If the array of identifiers 190 | # contains objects that are not present in the source app, the objects related to these identifiers are removed 191 | # from the target app. 192 | def replace_app_from_id(self, target_path, src_app_id, qIds=[""]): 193 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "ReplaceAppFromID", 194 | "params": {"qTargetAppId": target_path, "qSrcAppId": src_app_id, "qIds": qIds}}) 195 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 196 | try: 197 | return response['result'] 198 | except KeyError: 199 | return response['error'] 200 | 201 | # GetAuthenticatedUser 202 | # No parameters 203 | def get_auth_user(self): 204 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "GetAuthenticatedUser", "params": {}}) 205 | response_json = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 206 | try: 207 | return response_json["result"] 208 | except: 209 | return response_json["error"] 210 | 211 | # GetDatabasesFromConnectionString Lists the databases in a ODBC, OLEDB or CUSTOM data source (global level) 212 | # Parameters: qConnection (object - has several fields) qId: Identifier of the connection. Is generated by 213 | # the engine and is unique. qName (MANDATORY): Name of the connection. This parameter is mandatory and must 214 | # be set when creating or modifying a connection. qConnectionString (MANDATORY): One of: ODBC CONNECT TO [ 215 | # ], OLEDB CONNECT TO [], CUSTOM CONNECT TO [], "", "" Connection string. qType (MANDATORY): Type of the connection. One 217 | # of- ODBC, OLEDB, , folder, internet. For ODBC, OLEDB and custom 218 | # connections, the engine checks that the connection type matches the connection string. The type is not case 219 | # sensitive. qUserName: Name of the user who creates the connection. This parameter is optional; it is only 220 | # used for OLEDB, ODBC and CUSTOM connections. A call to GetConnection method does not return the user name. 221 | # qPassword: Password of the user who creates the connection. This parameter is optional; it is only used for 222 | # OLEDB, ODBC and CUSTOM connections. A call to GetConnection method does not return the password. 223 | # qModifiedDate: Is generated by the engine. Creation date of the connection or last modification date of the 224 | # connection. qMeta: Information about the connection. qLogOn (SSO Passthrough or not): Select which user 225 | # credentials to use to connect to the source. LOG_ON_SERVICE_USER: Disables, LOG_ON_CURRENT_USER: Enables 226 | def list_databases_from_odbc(self, connect_name, connect_string, connect_type, user_name, password, mod_date="", 227 | meta="", sso_passthrough="LOG_ON_SERVICE_USER"): 228 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "GetDatabasesFromConnectionString", 229 | "params": [{"qId": "", "qName": connect_name, "qConnectionString": connect_string, 230 | "qType": connect_type, "qUserName": user_name, "qPassword": password, 231 | "qModifiedDate": mod_date, "qMeta": meta, "qLogOn": sso_passthrough}]}) 232 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 233 | try: 234 | return response['result'] 235 | except KeyError: 236 | return response['error'] 237 | 238 | # IsValidConnectionString method: Checks if a connection string is valid. 239 | def is_valid_connect_string(self, connect_name, connect_string, connect_type, user_name, password, mod_date="", 240 | meta="", sso_passthrough="LOG_ON_SERVICE_USER"): 241 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "IsValidConnectionString", "params": [ 242 | {"qId": "", "qName": connect_name, "qConnectionString": connect_string, "qType": connect_type, 243 | "qUserName": user_name, "qPassword": password, "qModifiedDate": mod_date, "qMeta": meta, 244 | "qLogOn": sso_passthrough}]}) 245 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 246 | try: 247 | return response['result'] # Returns an array of databases 248 | except KeyError: 249 | return response['error'] 250 | 251 | # GetOdbcDsns: List all the ODBC connectors installed on the Sense server machine in Windows 252 | def get_odbc_dsns(self): 253 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "GetOdbcDsns", "params": {}}) 254 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 255 | try: 256 | return response['result'] 257 | except KeyError: 258 | return response['error'] 259 | 260 | # GetOleDbProviders: Returns the list of the OLEDB providers installed on the system. 261 | def get_ole_dbs(self): 262 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "GetOleDbProviders", "params": {}}) 263 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 264 | try: 265 | return response['result'] 266 | except KeyError: 267 | return response['error'] 268 | 269 | # GetProgress: Gives information about the progress of the DoReload and DoSave calls. Parameters: qRequestId: 270 | # Identifier of the DoReload or DoSave request or 0. Complete information is returned if the identifier of the 271 | # request is given. If the identifier is 0, less information is given. Progress messages and error messages are 272 | # returned but information like when the request started and finished is not returned. 273 | 274 | def get_progress(self, request_id): 275 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "GetProgress", "params": {}}) 276 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 277 | try: 278 | return response['result'] 279 | except KeyError: 280 | return response['error'] 281 | 282 | # IsDesktopMode: Indicates whether the user is working in Qlik Sense Desktop. 283 | # No parameters 284 | def is_desktop_mode(self, request_id): 285 | msg = json.dumps({"jsonrpc": "2.0", "id": 0, "handle": -1, "method": "IsDesktopMode", "params": {}}) 286 | response = json.loads(self.engine_socket.send_call(self.engine_socket, msg)) 287 | try: 288 | return response['result'] 289 | except KeyError: 290 | return response['error'] 291 | 292 | @staticmethod 293 | def get_doc_handle(doc_object): 294 | return doc_object['qHandle'] 295 | 296 | # ## NOT IMPLEMENTED, perceived out of use case scope: ## CreateDocEx, GetBaseBNFHash, GetBaseBNF, GetBNF, 297 | # GetCustomConnectors, GetDefaultAppFolder, GetFunctions, GetInteract, GetLogicalDriveStrings, 298 | # ## GetStreamList, GetSupportedCodePages, GetUniqueID, InteractDone, IsPersonalMode (deprecated), OSVersion, 299 | # ProductVersion (depr), QTProduct, QvVersion (depr), ## ReloadExtensionList, ReplaceAppFromID, 300 | -------------------------------------------------------------------------------- /engine_helper.py: -------------------------------------------------------------------------------- 1 | from structs import Structs 2 | import math 3 | from engine_app_api import EngineAppApi 4 | from engine_global_api import EngineGlobalApi 5 | from engine_generic_object_api import EngineGenericObjectApi 6 | from engine_field_api import EngineFieldApi 7 | import pandas as pd 8 | 9 | 10 | def getDataFrame(connection, appHandle, measures, dimensions, selections={}): 11 | engineGlobalApi = EngineGlobalApi(connection) 12 | ### Define Dimensions of hypercube 13 | hc_inline_dim = Structs.nx_inline_dimension_def(dimensions) 14 | 15 | ### Set sorting of Dimension by Measure 16 | hc_mes_sort = Structs.nx_sort_by() 17 | 18 | ### Define Measure of hypercube 19 | hc_inline_mes = Structs.nx_inline_measure_def(measures) 20 | 21 | ### Build hypercube from above definition 22 | hc_dim = Structs.nx_hypercube_dimensions(hc_inline_dim) 23 | hc_mes = Structs.nx_hypercube_measure(hc_mes_sort, hc_inline_mes) 24 | 25 | width = len(measures) + len(dimensions) 26 | height = int(math.floor(10000 / width)) 27 | nx_page = Structs.nx_page(0, 0, height, width) 28 | hc_def = Structs.hypercube_def("$", hc_dim, hc_mes, [nx_page]) 29 | 30 | engineAppApi = EngineAppApi(connection) 31 | hc_response = engineAppApi.create_object(appHandle, "CH01", "Chart", "qHyperCubeDef", hc_def) 32 | hc_handle = engineGlobalApi.get_handle(hc_response) 33 | 34 | engineGenericObjectApi = EngineGenericObjectApi(connection) 35 | 36 | engineFieldApi = EngineFieldApi(connection) 37 | 38 | for field in selections.keys(): 39 | fieldHandle = engineGlobalApi.get_handle(engineAppApi.get_field(appHandle, field)) 40 | values = [] 41 | for selectedValue in selections[field]: 42 | values.append({'qText': selectedValue}) 43 | 44 | engineFieldApi.select_values(fieldHandle, values) 45 | 46 | i = 0 47 | while i % height == 0: 48 | nx_page = Structs.nx_page(i, 0, height, width) 49 | hc_data = engineGenericObjectApi.get_hypercube_data(hc_handle, "/qHyperCubeDef", [nx_page]) 50 | elems = hc_data["qDataPages"][0]['qMatrix'] 51 | 52 | df = pd.DataFrame() 53 | 54 | for elem in elems: 55 | j = 0 56 | for dim in dimensions: 57 | df.set_value(i, dim, elem[j]["qText"]) 58 | j += 1 59 | for meas in measures: 60 | df.set_value(i, meas, elem[j]["qNum"]) 61 | j += 1 62 | 63 | i += 1 64 | 65 | return df 66 | -------------------------------------------------------------------------------- /pyqlikengine.py: -------------------------------------------------------------------------------- 1 | import engine_app_api, engine_communicator, engine_field_api, engine_generic_object_api, engine_global_api, structs 2 | 3 | 4 | class QixEngine: 5 | 6 | def __init__(self, url, is_secure=False, proxy_prefix='', user_directory='', user_id='', private_key_path='', 7 | ignore_cert_errors=False): 8 | self.url = url 9 | if is_secure: 10 | self.conn = engine_communicator.SecureEngineCommunicator(url, proxy_prefix, user_directory,user_id, 11 | private_key_path, ignore_cert_errors) 12 | else: 13 | self.conn = engine_communicator.EngineCommunicator(url) 14 | self.ega = engine_global_api.EngineGlobalApi(self.conn) 15 | self.eaa = engine_app_api.EngineAppApi(self.conn) 16 | self.egoa = engine_generic_object_api.EngineGenericObjectApi(self.conn) 17 | self.efa = engine_field_api.EngineFieldApi(self.conn) 18 | self.Structs = structs.Structs() 19 | self.app_handle = '' 20 | 21 | def create_app(self, app_name='my_app'): 22 | app = self.ega.create_app(app_name) 23 | try: 24 | return app['qAppId'] 25 | except KeyError: 26 | return app['message'] 27 | 28 | def load_script(self, script): 29 | self.eaa.set_script(self.app_handle, script) 30 | return self.eaa.do_reload_ex(self.app_handle)['qResult']['qSuccess'] 31 | 32 | def open_app(self, app_obj): 33 | opened_app = self.ega.open_doc(app_obj)['qReturn'] 34 | self.app_handle = self.ega.get_handle(opened_app) 35 | return opened_app['qGenericId'] 36 | 37 | def create_hypercube(self, list_of_dimensions=[], list_of_measures=[], rows_to_return=1000): 38 | no_of_columns = len(list_of_dimensions) + len(list_of_measures) 39 | hc_dim = [] 40 | for d in list_of_dimensions: 41 | hc_inline_dim = self.Structs.nx_inline_dimension_def([d]) 42 | hc_dim.append(self.Structs.nx_hypercube_dimensions(hc_inline_dim)) 43 | hc_mes = [] 44 | for m in list_of_measures: 45 | hc_mes_sort = self.Structs.nx_sort_by() 46 | hc_inline_mes = self.Structs.nx_inline_measure_def(m) 47 | hc_mes.append(self.Structs.nx_hypercube_measure(hc_mes_sort, hc_inline_mes)) 48 | nx_page = self.Structs.nx_page(0, 0, rows_to_return, no_of_columns) 49 | hc_def = self.Structs.hypercube_def("$", hc_dim, hc_mes, [nx_page]) 50 | hc_response = self.eaa.create_object(self.app_handle, "CH01", "Chart", "qHyperCubeDef", hc_def) 51 | hc_handle = self.ega.get_handle(hc_response["qReturn"]) 52 | self.egoa.get_layout(hc_handle) 53 | hc_data = self.egoa.get_hypercube_data(hc_handle, "/qHyperCubeDef", [nx_page]) 54 | no_of_columns = len(list_of_dimensions)+len(list_of_measures) 55 | return hc_data, no_of_columns 56 | 57 | @staticmethod 58 | def convert_hypercube_to_matrix(hc_data, no_of_columns): 59 | rows = hc_data["qDataPages"][0]['qMatrix'] 60 | matrix = [[0 for x in range(no_of_columns)] for y in range(len(rows))] 61 | for col_idx, row in enumerate(rows): 62 | for cell_idx, cell_val in enumerate(row): 63 | matrix[col_idx][cell_idx] = cell_val['qText'] 64 | return [list(i) for i in zip(*matrix)] 65 | 66 | @staticmethod 67 | def convert_hypercube_to_inline_table(hc_data, table_name): 68 | rows = hc_data["qDataPages"][0]['qMatrix'] 69 | script = str.format('{0}:{1}Load * Inline [{1}', table_name, '\n') 70 | inline_rows = '' 71 | header_row = '' 72 | for col_idx in range(len(rows[0])): 73 | header_row = header_row + str.format('Column{0}{1}', col_idx, ',') 74 | header_row = header_row[:-1] + '\n' 75 | for row in rows: 76 | for cell_val in row: 77 | inline_rows = inline_rows + "'" + cell_val['qText'] + "'" + ',' 78 | inline_rows = inline_rows[:-1] + '\n' 79 | return script + header_row + inline_rows + '];' 80 | 81 | def select_in_dimension(self,dimension_name, list_of_values): 82 | lb_field = self.eaa.get_field(self.app_handle, dimension_name) 83 | fld_handle = self.ega.get_handle(lb_field["qReturn"]) 84 | values_to_select = [] 85 | for val in list_of_values: 86 | val = {'qText': val} 87 | values_to_select.append(val) 88 | return self.efa.select_values(fld_handle, values_to_select) 89 | 90 | def select_excluded_in_dimension(self, dimension_name): 91 | lb_field = self.eaa.get_field(self.app_handle, dimension_name) 92 | fld_handle = self.ega.get_handle(lb_field["qReturn"]) 93 | return self.efa.select_excluded(fld_handle) 94 | 95 | def select_possible_in_dimension(self, dimension_name): 96 | lb_field = self.eaa.get_field(self.app_handle, dimension_name) 97 | fld_handle = self.ega.get_handle(lb_field["qReturn"]) 98 | return self.efa.select_possible(fld_handle) 99 | 100 | # return a list of tuples where first value in tuple is the actual data value and the second tuple value is that 101 | # values selection state 102 | def get_list_object_data(self, dimension_name): 103 | lb_field = self.eaa.get_field(self.app_handle, dimension_name) 104 | fld_handle = self.ega.get_handle(lb_field["qReturn"]) 105 | nx_page = self.Structs.nx_page(0, 0, self.efa.get_cardinal(fld_handle)["qReturn"]) 106 | lb_def = self.Structs.list_object_def("$", "", [dimension_name], None, None, [nx_page]) 107 | lb_param = {"qInfo": {"qId": "SLB01", "qType": "ListObject"}, "qListObjectDef": lb_def} 108 | listobj_handle = self.eaa.create_session_object(self.app_handle, lb_param)["qReturn"]["qHandle"] 109 | val_list = self.egoa.get_layout(listobj_handle)["qLayout"]["qListObject"]["qDataPages"][0]["qMatrix"] 110 | val_n_state_list=[] 111 | for val in val_list: 112 | val_n_state_list.append((val[0]["qText"],val[0]["qState"])) 113 | return val_n_state_list 114 | 115 | def clear_selection_in_dimension(self, dimension_name): 116 | lb_field = self.eaa.get_field(self.app_handle, dimension_name) 117 | fld_handle = self.ega.get_handle(lb_field["qReturn"]) 118 | return self.efa.clear(fld_handle)['qReturn'] 119 | 120 | def clear_all_selections(self): 121 | return self.eaa.clear_all(self.app_handle, True) 122 | 123 | def delete_app(self, app_name): 124 | return self.ega.delete_app(app_name)['qSuccess'] 125 | 126 | def disconnect(self): 127 | self.conn.close_qvengine_connection(self.conn) 128 | -------------------------------------------------------------------------------- /structs.py: -------------------------------------------------------------------------------- 1 | class Structs: 2 | def __init__(self): 3 | pass 4 | 5 | @staticmethod 6 | def list_object_def(state_name="$", library_id="", field_defs=None, field_labels=None, sort_criterias=None, 7 | initial_data_fetch=None): 8 | if initial_data_fetch is None: 9 | initial_data_fetch = [] 10 | if sort_criterias is None: 11 | sort_criterias = [] 12 | if field_labels is None: 13 | field_labels = [] 14 | if field_defs is None: 15 | field_defs = [] 16 | return {"qStateName": state_name, 17 | "qLibraryId": library_id, 18 | "qDef": { 19 | "qFieldDefs": field_defs, 20 | "qFieldLabels": field_labels, 21 | "qSortCriterias": sort_criterias 22 | }, 23 | "qInitialDataFetch": initial_data_fetch 24 | } 25 | 26 | @staticmethod 27 | def hypercube_def(state_name="$", nx_dims=[], nx_meas=[], nx_page=[], inter_column_sort=[0, 1, 2], suppress_zero=False, 28 | suppress_missing=False): 29 | return {"qStateName": state_name, 30 | "qDimensions": nx_dims, # NxDimensions 31 | "qMeasures": nx_meas, # NxMeasure 32 | "qInterColumnSortOrder": inter_column_sort, 33 | "qSuppressZero": suppress_zero, 34 | "qSuppressMissing": suppress_missing, 35 | "qInitialDataFetch": nx_page, # NxPage 36 | "qMode": 'S', 37 | "qNoOfLeftDims": -1, 38 | "qAlwaysFullyExpanded": False, 39 | "qMaxStackedCells": 5000, 40 | "qPopulateMissing": False, 41 | "qShowTotalsAbove": False, 42 | "qIndentMode": False, 43 | "qCalcCond": "", 44 | "qSortbyYValue": 0 45 | } 46 | 47 | @staticmethod 48 | def nx_hypercube_dimensions(dim_def): 49 | return {"qLibraryId": "", 50 | "qNullSuppression": False, 51 | "qDef": dim_def 52 | } 53 | 54 | @staticmethod 55 | def nx_inline_dimension_def(field_definitions=[], grouping='N', field_labels=[]): 56 | return {"qGrouping": grouping, 57 | "qFieldDefs": field_definitions, 58 | "qFieldLabels": field_labels 59 | } 60 | 61 | @staticmethod 62 | def nx_hypercube_measure(sort_by={}, nx_inline_measures_def=""): 63 | return {"qSortBy": sort_by, 64 | "qDef": nx_inline_measures_def 65 | } 66 | 67 | @staticmethod 68 | def nx_sort_by(state=0, freq=0, numeric=0, ascii=0, load_order=1): 69 | return {"qSortByState": state, 70 | "qSortByFrequency": freq, 71 | "qSortByNumeric": numeric, 72 | "qSortByAscii": ascii, 73 | "qSortByLoadOrder": load_order, 74 | "qSortByExpression": 0, 75 | "qExpression": { 76 | "qv": "" 77 | } 78 | } 79 | 80 | @staticmethod 81 | def nx_inline_measure_def(definition, label="", description="", tags=[], grouping="N"): 82 | return {"qLabel": label, 83 | "qDescription": description, 84 | "qTags": tags, 85 | "qGrouping": grouping, 86 | "qDef": definition 87 | } 88 | 89 | @staticmethod 90 | def nx_page(top=0, left=0, height=2, width=2): 91 | return {"qTop": top, 92 | "qLeft": left, 93 | "qHeight": height, 94 | "qWidth": width 95 | } -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qliknln/pyqlikengine/720c2558cd88c0c6eab70a691b4261c70f5194d5/test/__init__.py -------------------------------------------------------------------------------- /test/test_app_api.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from engine_app_api import EngineAppApi 4 | from engine_communicator import EngineCommunicator 5 | from engine_field_api import EngineFieldApi 6 | from engine_global_api import EngineGlobalApi 7 | from structs import Structs 8 | 9 | from engine_generic_object_api import EngineGenericObjectApi 10 | 11 | 12 | class TestAppApi(unittest.TestCase): 13 | 14 | # Constructor to prepare everything before running the tests. 15 | def setUp(self): 16 | url = 'ws://localhost:4848/app' 17 | self.conn = EngineCommunicator(url) 18 | self.ega = EngineGlobalApi(self.conn) 19 | self.eaa = EngineAppApi(self.conn) 20 | self.egoa = EngineGenericObjectApi(self.conn) 21 | self.efa = EngineFieldApi(self.conn) 22 | self.struct = Structs() 23 | self.app = self.ega.create_app("TestApp")['qAppId'] 24 | opened_app = self.ega.open_doc(self.app) 25 | self.app_handle = self.ega.get_handle(opened_app['qReturn']) 26 | 27 | def test_add_alternate_state(self): 28 | response = self.eaa.add_alternate_state(self.app_handle,"MyState") 29 | self.assertEqual(response, {}, "Failed to add alternate state") 30 | 31 | def test_create_hypercube_object(self): 32 | with open('./test/test_data/ctrl00_script.qvs') as f: 33 | script = f.read() 34 | self.eaa.set_script(self.app_handle,script) 35 | self.eaa.do_reload_ex(self.app_handle) 36 | 37 | #Create the inline dimension structures 38 | hc_inline_dim1 = Structs.nx_inline_dimension_def(["Alpha"]) 39 | hc_inline_dim2 = Structs.nx_inline_dimension_def(["Num"]) 40 | 41 | #Create a sort structure 42 | hc_mes_sort = Structs.nx_sort_by() 43 | 44 | #Create the measure structures 45 | hc_inline_mes1 = Structs.nx_inline_measure_def("=Sum(Num)") 46 | hc_inline_mes2 = Structs.nx_inline_measure_def("=Avg(Num)") 47 | 48 | #Create hypercube dimensions from the inline dimension structures 49 | hc_dim1 = Structs.nx_hypercube_dimensions(hc_inline_dim1) 50 | hc_dim2 = Structs.nx_hypercube_dimensions(hc_inline_dim2) 51 | 52 | # Create hypercube measures from the inline measure structures 53 | hc_mes1 = Structs.nx_hypercube_measure(hc_mes_sort, hc_inline_mes1) 54 | hc_mes2 = Structs.nx_hypercube_measure(hc_mes_sort, hc_inline_mes2) 55 | 56 | # Create the paging model/structure (26 rows and 4 columns) 57 | nx_page = Structs.nx_page(0, 0, 26, 4) 58 | 59 | # Create a hypercube definition with arrays of hc dims, measures and nxpages 60 | hc_def = Structs.hypercube_def("$", [hc_dim1, hc_dim2], [hc_mes1, hc_mes2], [nx_page]) 61 | 62 | # Create a Chart object with the hypercube definitions as parameter 63 | hc_response = self.eaa.create_object(self.app_handle, "CH01", "Chart", "qHyperCubeDef", hc_def) 64 | 65 | #Get the handle to the chart object (this may be different in my local repo. I have made some changes to this 66 | # for future versions) 67 | hc_handle = self.ega.get_handle(hc_response['qReturn']) 68 | 69 | # Validate the chart object by calling get_layout 70 | self.egoa.get_layout(hc_handle) 71 | 72 | # Call the get_hypercube_data to get the resulting json object, using the handle and nx page as paramters 73 | hc_data = self.egoa.get_hypercube_data(hc_handle,"/qHyperCubeDef",[nx_page]) 74 | 75 | self.assertTrue(type(hc_data is {}), "Unexpected type of hypercube data") 76 | first_element_number = hc_data["qDataPages"][0]["qMatrix"][0][0]["qElemNumber"] 77 | first_element_text = hc_data["qDataPages"][0]["qMatrix"][0][0]["qText"] 78 | self.assertTrue(first_element_number == 0, "Incorrect value in first element number") 79 | self.assertTrue(first_element_text == 'A', "Incorrect value in first element text") 80 | 81 | def tearDown(self): 82 | self.ega.delete_app(self.app) 83 | self.conn.close_qvengine_connection(self.conn) 84 | 85 | 86 | if __name__ == '__main__': 87 | unittest.main() 88 | -------------------------------------------------------------------------------- /test/test_data/ctrl00_script.qvs: -------------------------------------------------------------------------------- 1 | SET ThousandSep=' '; 2 | SET DecimalSep=','; 3 | SET MoneyThousandSep=' '; 4 | SET MoneyDecimalSep=','; 5 | SET MoneyFormat='# ##0,00 kr;-# ##0,00 kr'; 6 | SET TimeFormat='hh:mm:ss'; 7 | SET DateFormat='YYYY-MM-DD'; 8 | SET TimestampFormat='YYYY-MM-DD hh:mm:ss[.fff]'; 9 | SET FirstWeekDay=0; 10 | SET BrokenWeeks=0; 11 | SET ReferenceDay=4; 12 | SET FirstMonthOfYear=1; 13 | SET CollationLocale='en-SE'; 14 | SET CreateSearchIndexOnReload=1; 15 | SET MonthNames='Jan;Feb;Mar;Apr;May;Jun;Jul;Aug;Sep;Oct;Nov;Dec'; 16 | SET LongMonthNames='January;February;March;April;May;June;July;August;September;October;November;December'; 17 | SET DayNames='Mon;Tue;Wed;Thu;Fri;Sat;Sun'; 18 | SET LongDayNames='Monday;Tuesday;Wednesday;Thursday;Friday;Saturday;Sunday'; 19 | 20 | Characters: 21 | Load Chr(RecNo()+Ord('A')-1) as Alpha, RecNo() as Num autogenerate 26; 22 | 23 | ASCII: 24 | Load 25 | if(RecNo()>=65 and RecNo()<=90,RecNo()-64) as Num, 26 | Chr(RecNo()) as AsciiAlpha, 27 | RecNo() as AsciiNum 28 | autogenerate 255 29 | Where (RecNo()>=32 and RecNo()<=126) or RecNo()>=160 ; 30 | 31 | Transactions: 32 | Load 33 | TransLineID, 34 | TransID, 35 | mod(TransID,26)+1 as Num, 36 | Pick(Ceil(3*Rand1),'A','B','C') as Dim1, 37 | Pick(Ceil(6*Rand1),'a','b','c','d','e','f') as Dim2, 38 | Pick(Ceil(3*Rand()),'X','Y','Z') as Dim3, 39 | Round(1000*Rand()*Rand()*Rand1) as Expression1, 40 | Round( 10*Rand()*Rand()*Rand1) as Expression2, 41 | Round(Rand()*Rand1,0.00001) as Expression3; 42 | Load 43 | Rand() as Rand1, 44 | IterNo() as TransLineID, 45 | RecNo() as TransID 46 | Autogenerate 1000 47 | While Rand()<=0.5 or IterNo()=1; 48 | 49 | Comment Field Dim1 With "This is a field comment"; 50 | -------------------------------------------------------------------------------- /test/test_field_api.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from engine_app_api import EngineAppApi 4 | from engine_communicator import EngineCommunicator 5 | from engine_field_api import EngineFieldApi 6 | from engine_global_api import EngineGlobalApi 7 | from structs import Structs 8 | 9 | from engine_generic_object_api import EngineGenericObjectApi 10 | 11 | 12 | class TestFieldApi(unittest.TestCase): 13 | 14 | def setUp(self): 15 | url = 'ws://localhost:4848/app' 16 | self.conn = EngineCommunicator(url) 17 | self.ega = EngineGlobalApi(self.conn) 18 | self.eaa = EngineAppApi(self.conn) 19 | self.egoa = EngineGenericObjectApi(self.conn) 20 | self.efa = EngineFieldApi(self.conn) 21 | self.struct = Structs() 22 | self.app = self.ega.create_app("TestApp")["qAppId"] 23 | opened_app = self.ega.open_doc(self.app) 24 | self.app_handle = self.ega.get_handle(opened_app['qReturn']) 25 | with open('./test/test_data/ctrl00_script.qvs') as f: 26 | script = f.read() 27 | self.eaa.set_script(self.app_handle, script) 28 | self.eaa.do_reload_ex(self.app_handle) 29 | nx_page_initial = Structs.nx_page(0, 0, 26, 1) 30 | self.lb_def = Structs.list_object_def("$","",["Alpha"],None,None,[nx_page_initial]) 31 | self.lb_param = {"qInfo":{"qId": "SLB01", "qType": "ListObject"}, "qListObjectDef": self.lb_def} 32 | self.lb_sobject = self.eaa.create_session_object(self.app_handle, self.lb_param) 33 | self.lb_handle = self.ega.get_handle(self.lb_sobject["qReturn"]) 34 | self.egoa.get_layout(self.lb_handle) 35 | self.lb_field = self.eaa.get_field(self.app_handle, "Alpha") 36 | self.fld_handle = self.ega.get_handle(self.lb_field["qReturn"]) 37 | 38 | def test_select_values(self): 39 | values_to_select = [{'qText': 'A'}, {'qText': 'B'}, {'qText': 'C'}] 40 | sel_res = self.efa.select_values(self.fld_handle,values_to_select) 41 | self.assertTrue(sel_res["qReturn"] is True, "Failed to perform selection") 42 | val_mtrx = self.egoa.get_layout(self.lb_handle)["qLayout"]["qListObject"]["qDataPages"][0]["qMatrix"] 43 | self.assertEqual(val_mtrx[0][0]["qState"],"S","Failed to select first value") 44 | self.assertEqual(val_mtrx[4][0]["qState"], "X", "Failed to exclude fifth value") 45 | self.eaa.clear_all(self.app_handle) 46 | val_mtrx = self.egoa.get_layout(self.lb_handle)["qLayout"]["qListObject"]["qDataPages"][0]["qMatrix"] 47 | self.assertEqual(val_mtrx[0][0]["qState"], "O", "Failed to clear selection") 48 | self.assertEqual(val_mtrx[4][0]["qState"], "O", "Failed to clear selection") 49 | 50 | def tearDown(self): 51 | self.ega.delete_app(self.app) 52 | self.conn.close_qvengine_connection(self.conn) 53 | 54 | 55 | if __name__ == '__main__': 56 | unittest.main() -------------------------------------------------------------------------------- /test/test_global_api.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import unittest 3 | 4 | from engine_app_api import EngineAppApi 5 | from engine_communicator import EngineCommunicator 6 | from engine_field_api import EngineFieldApi 7 | from engine_global_api import EngineGlobalApi 8 | from structs import Structs 9 | 10 | from engine_generic_object_api import EngineGenericObjectApi 11 | 12 | 13 | # Unittest class for the methods in EngineGlobalApi. All tests methods must have the "test_" prefix. 14 | 15 | 16 | class TestGlobalApi(unittest.TestCase): 17 | # Constructor to prepare everything before running the tests. 18 | def setUp(self): 19 | url = 'ws://localhost:4848/app' 20 | self.conn = EngineCommunicator(url) 21 | self.ega = EngineGlobalApi(self.conn) 22 | self.eaa = EngineAppApi(self.conn) 23 | self.egoa = EngineGenericObjectApi(self.conn) 24 | self.efa = EngineFieldApi(self.conn) 25 | self.struct = Structs() 26 | 27 | def test_get_doclist(self): 28 | response = self.ega.get_doc_list() 29 | self.assertTrue(len(response) > 0) 30 | 31 | def test_app_methods(self): 32 | response_create = self.ega.create_app("test_app")['qAppId'] 33 | self.assertTrue(response_create.endswith(".qvf"), "Failed to create app. Response did not end with .qvf") 34 | #response_copy = self.ega.copy_app("test_app_copy", response_create) 35 | #print response_copy 36 | response_open = self.ega.open_doc("test_app") 37 | #response_open = self.ega.open_doc_ex("test_app_asdf") 38 | self.assertEqual(response_open['qReturn']["qHandle"], 1, 39 | "Failed to retrieve a proper document handle with open_doc method") 40 | self.assertTrue(response_open['qReturn']["qGenericId"].endswith(".qvf"), 41 | 'Generic id does not contain any app file extension using open_doc method') 42 | self.assertEqual(response_open['qReturn']["qType"],"Doc",'Unknown doc type returned using open_doc method') 43 | response_get_active_doc = self.ega.get_active_doc() 44 | self.assertEqual(response_get_active_doc['qReturn']["qHandle"], 1, "Failed to retrive a proper document handle with " 45 | "get_active_doc method") 46 | self.assertTrue(response_get_active_doc['qReturn']["qGenericId"].endswith(".qvf"), 47 | 'Generic id does not contain any app file extension using get_active_doc method') 48 | self.assertEqual(response_get_active_doc['qReturn']["qType"], "Doc", 'Unknown doc type returned using get_active_doc ' 49 | 'method') 50 | response_delete = self.ega.delete_app(response_create)['qSuccess'] 51 | #self.ega.delete_app(response_copy) 52 | self.assertTrue(response_delete, "Failed to delete app") 53 | 54 | # May be a meaningless test since there are no commands to abort?? 55 | def test_abort_all(self): 56 | response = self.ega.abort_all() 57 | self.assertEqual(response,{},'abort_all method returned unexpected object') 58 | 59 | # May be a meaningless test since there is no request with id 1? 60 | def test_abort_request(self): 61 | response = self.ega.abort_request(1) 62 | self.assertEqual(response, {}, 'abort_request method returned unexpected object') 63 | 64 | def test_configure_reload(self): 65 | response_pos = self.ega.configure_reload(True, True, True) 66 | self.assertEqual(response_pos, {}, 'configure_reload method returned unexpected object') 67 | response_neg = self.ega.configure_reload('dummy',True,True)['message'] 68 | self.assertEqual(response_neg, "Invalid method parameter(s)") 69 | 70 | def test_create_session_app(self): 71 | response = self.ega.create_session_app()['qSessionAppId'] 72 | self.assertTrue(response.startswith("SessionApp_"),"Failed to create session app") 73 | 74 | def test_create_session_app_from_app(self): 75 | response_create = self.ega.create_app("test_app")['qAppId'] 76 | response = self.ega.create_session_app_from_app(response_create)['qSessionAppId'] 77 | self.ega.delete_app(response_create) 78 | self.assertTrue(response.startswith("SessionApp_"),"Failed to create session app") 79 | 80 | def test_export_app(self): 81 | tmp_folder = tempfile.gettempdir() 82 | response_create = self.ega.create_app("test_app")['qAppId'] 83 | response = self.ega.export_app(tmp_folder,response_create) 84 | self.ega.delete_app(response_create) 85 | print("BUG returns method not found. Reported") 86 | 87 | def test_replace_app_from_id(self): 88 | response_create = self.ega.create_app("test_app")['qAppId'] 89 | tmp_folder = tempfile.gettempdir() 90 | response = self.ega.replace_app_from_id(tmp_folder, response_create) 91 | print("Same bug as CopyApp and ExportApp") 92 | self.ega.delete_app(response_create) 93 | 94 | def test_get_auth_user(self): 95 | response = self.ega.get_auth_user() 96 | self.assertTrue(type(response) is dict, "Failed to retrieve authenticated user") 97 | 98 | def test_is_desktop_mode(self): 99 | response = self.ega.is_desktop_mode(0)['qReturn'] 100 | self.assertTrue(type(response) is bool,'Failed to check desktop mode') 101 | 102 | # Clean up after the tests have been run 103 | def tearDown(self): 104 | self.conn.close_qvengine_connection(self.conn) 105 | 106 | 107 | if __name__ == '__main__': 108 | unittest.main() 109 | 110 | # def make_selection(self): 111 | # self.ega.open_doc('SelectApp') 112 | # doc = self.ega.get_active_doc() 113 | # time.sleep(1) 114 | # h=self.ega.get_handle(doc) 115 | # lod = self.struct.list_object_def("$","",["Alpha"],["my field"],[{"qSortByLoadOrder": 1}],[{"qTop": 0, "qLeft": 0, "qHeight": 3, "qWidth": 1}]) 116 | # lobj= self.eaa.create_object(h,"LB01","ListObject","qListObjectDef",lod) 117 | # h3=self.ega.get_handle(lobj['qReturn']) 118 | # self.egoa.get_layout(h3) 119 | # fld = self.eaa.get_field(h, 'Alpha') 120 | # h2 = self.ega.get_handle(fld) 121 | # val = [{"qText": "A"}, {"qText": "B"}] 122 | # self.efa.select_values(h2,val) 123 | # print self.egoa.get_layout(h3) 124 | 125 | 126 | # conn.connect() 127 | # print ega.get_doc_list() 128 | # print conn.get_os_name() 129 | # ega.create_app('theApp') 130 | # time.sleep(1) 131 | # ega.open_doc('theApp') 132 | # doc = ega.get_active_doc() 133 | # time.sleep(1) 134 | # handle = ega.get_doc_handle(doc) 135 | # eaa.set_script(handle,'Load RecNo() as Field autogenerate 10;') 136 | # print eaa.do_reload(handle) 137 | # print eaa.do_reload_ex(handle) 138 | # ega.delete_app('C:\\Users\\Niklas\\Documents\\Qlik\\Sense\\Apps\\theApp.qvf') 139 | # conn.close_qvengine_connection(conn) 140 | -------------------------------------------------------------------------------- /test/test_labs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pyqlikengine import QixEngine 3 | import os 4 | 5 | 6 | class TestLabs(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.qixe = QixEngine('ws://localhost:4848/app') 10 | 11 | def test_select_in_field(self): 12 | print ('sdfasef') 13 | app = os.path.join("C:/", "Users", "nln", "Documents", "Qlik", "Sense", "Apps", "Consumer Sales.qvf") 14 | self.qixe.open_app(app) 15 | print(self.qixe.select_in_dimension('Product Sub Group', ['Cheese'])) 16 | 17 | def tearDown(self): 18 | pass 19 | 20 | 21 | if __name__ == '__main__': 22 | unittest.main() -------------------------------------------------------------------------------- /test/test_pyqlikengine.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from pyqlikengine import QixEngine 3 | 4 | 5 | class TestQixEngine(unittest.TestCase): 6 | 7 | def setUp(self): 8 | self.qixe = QixEngine('ws://localhost:4848/app') 9 | app = self.qixe.create_app('test_app') 10 | self.assertTrue(app.endswith('.qvf'), 'Failed to create app') 11 | app_exists = self.qixe.create_app('test_app') 12 | self.assertTrue(app_exists == "App already exists", 'Failed to handle existing app exception') 13 | self.opened_app = self.qixe.open_app(app) 14 | with open('./test_data/ctrl00_script.qvs') as f: 15 | script = f.read() 16 | self.assertTrue(self.qixe.load_script(script), 'Failed to load script') 17 | 18 | def test_create_hypercube(self): 19 | hc = self.qixe.create_hypercube(['Dim1', 'Dim2'], ['=Sum(Expression1)', '=Sum(Expression2)', '=Sum(Expression3)']) 20 | hc_cols = self.qixe.convert_hypercube_to_matrix(hc[0], hc[1]) 21 | self.assertTrue(len(hc_cols) == 5, 'Failed to return proper number of columns') 22 | self.inline_table = self.qixe.convert_hypercube_to_inline_table(hc[0], 'MyTable') 23 | self.assertTrue(self.inline_table.startswith('MyTable'), 'Failed to create inline statement from hypercube') 24 | self.mtrx = self.qixe.convert_hypercube_to_matrix(hc[0], hc[1]) 25 | self.assertTrue(len(self.mtrx) == 5, 'Failed to create matrix from hypercube') 26 | 27 | def test_select_clear_in_dimension(self): 28 | select_result = self.qixe.select_in_dimension('Alpha', ['A', 'C', 'E']) 29 | self.assertTrue(select_result["change"] == [1, 2], "Failed to select values") 30 | self.assertTrue(select_result["result"]['qReturn'], "Failed to select values") 31 | self.assertTrue(self.qixe.clear_selection_in_dimension('Alpha'),'Failed to clear selection') 32 | 33 | def test_select_clear_all_in_dimension(self): 34 | select_result = self.qixe.select_in_dimension('Alpha', ['A', 'C', 'E']) 35 | self.assertTrue(select_result["change"] == [1, 2], "Failed to select values") 36 | self.assertTrue(select_result["result"]['qReturn'], "Failed to select values") 37 | self.qixe.clear_all_selections() 38 | 39 | def test_select_excluded(self): 40 | self.qixe.select_in_dimension('Alpha', ['A', 'C', 'E']) 41 | select_result = self.qixe.select_excluded_in_dimension('Alpha') 42 | self.assertTrue(select_result['qReturn'], 'Failed to select excluded') 43 | 44 | def test_select_possible(self): 45 | select_result = self.qixe.select_possible_in_dimension('Alpha') 46 | self.assertTrue(select_result['qReturn'], 'Failed to select possible') 47 | 48 | def test_get_list_object_data(self): 49 | self.assertTrue(len(self.qixe.get_list_object_data('Alpha')) == 26, 'Failed to get value list') 50 | 51 | def tearDown(self): 52 | self.assertTrue(self.qixe.delete_app(self.opened_app), 'Failed to delete app') 53 | 54 | 55 | if __name__ == '__main__': 56 | unittest.main() 57 | --------------------------------------------------------------------------------