Search code examples
javascriptpythonjsonwebsocketqliksense

Export visualization data using Qlik Engine JSON API


Our organization uses Qlik Sense Enterprise and we are looking to automate the download process of the data used for visualizations (format can be excel or csv) instead of the manual process which leads to the following (cropped screenshot shown):

From qlik sense portal

For our use case, lets assume there is only one app which has one sheet inside and that sheet has 3 visualizations.

I have written a python script which currently has been connected to the localhost and I am able to retrieve the app_id, sheet_id and the id of the 3 charts present using Qlik Engine JSON API. The code works in the following manner:

  1. Fetch the doc_list (app_list)
  2. Select the app, since we have only one app we choose index as 0
  3. Create a session object (I saw Qlik Engine on Dev Hub exhibiting this behavior which is why I executed this step)
  4. Get the layout of the app
  5. Select the sheet, since we have one sheet we choose index as 0
  6. Iterate through the visualizations and print their names

I have provided the code below for your reference as well as the pastebin link can be accessed here

import requests
import websocket, ssl
import json,  csv
import os, time
from pprint import pprint 
 
#Connecting to the server. The lines below will be replaced by the certificates and headers for enterprise usage later
ws = websocket.WebSocket()
ws.connect("ws://localhost:4848/app/")
 
#For getting the doc (app) list
doclist_req = {
    "handle": -1,
    "method": "GetDocList",
    "params": [],
    "outKey": -1,
    "id": 1
}
 
ws.send(json.dumps(doclist_req))
result = ws.recv()
ws.send(json.dumps(doclist_req))
result = ws.recv()
result_json = json.loads(result)
print(result_json)
print()
 
#For opening the doc (app)
app_req = {
    "jsonrpc": "2.0",
    "method": "OpenDoc",
    "handle": -1,
    "params": [
        #Can iterate if multiple apps are there
        #Since only one app was present we used the index 0
        result_json['result']['qDocList'][0]['qDocId']
    ],
    "outKey": -1,
    "id": 2
}
 
#The first call seems to be for establishing the connection and second to actually send the request body
ws.send(json.dumps(app_req))
result = ws.recv()
ws.send(json.dumps(app_req))
result = ws.recv()
result_json = json.loads(result)
print(result_json)
print()
app_req_handle = result_json['result']['qReturn']['qHandle']
 
#For creating the session object necessary for fetching dimensions, fields, etc.
session_req = {
    "jsonrpc": "2.0",
    "method": "CreateSessionObject",
    "handle": app_req_handle,
    "params": [
        {
            "qInfo": {
                "qType": "SheetList"
            },
            "qAppObjectListDef": {
                "qType": "sheet",
                "qData": {
                    "title": "/qMetaDef/title",
                    "description": "/qMetaDef/description",
                    "thumbnail": "/thumbnail",
                    "cells": "/cells",
                    "rank": "/rank",
                    "columns": "/columns",
                    "rows": "/rows"
                }
            }
        }
    ],
    "outKey": -1,
    "id": 3
}
 
ws.send(json.dumps(session_req))
result = ws.recv()
ws.send(json.dumps(session_req))
result = ws.recv()
result_json = json.loads(result)
print(result_json)
print()
session_req_handle = result_json['result']['qReturn']['qHandle']
 
#For fetching the layout of the sheets
layout_req = {
    "jsonrpc": "2.0",
    "method": "GetLayout",
    "handle": session_req_handle,
    "params": [],
    "outKey": -1,
    "id": 4
}
 
ws.send(json.dumps(layout_req))
result = ws.recv()
ws.send(json.dumps(layout_req))
result = ws.recv()
result_json = json.loads(result)
print(result_json)
print()
 
#Since only one sheet was present we used the index 0
list_of_charts = result_json['result']['qLayout']['qAppObjectList']['qItems'][0]['qData']['cells']
for chart in list_of_charts:
    print(chart['name'])
print()
 
ws.close()

I have explored a lot of pages on the Qlik community as well as SO and ExportData seems to be the way to go but I am not able to write the correct JSON request for it. For reference, the output I get in my terminal on executing the python script is given below. I am quite new to this and I would be very thankful for any help from all of you.

{'jsonrpc': '2.0', 'id': 1, 'result': {'qDocList': [{'qDocName': 'Test_2.qvf', 'qConnectedUsers': 0, 'qFileTime': 44188.190983796296, 'qFileSize': 851968, 'qDocId': 'C:\Users\mohdm\Documents\Qlik\Sense\Apps\Test_2.qvf', 'qMeta': {'hassectionaccess': False, 'encrypted': False}, 'qLastReloadTime': '2020-12-22T19:12:22.245Z', 'qTitle': 'Test_2', 'qThumbnail': {}}]}}

{'jsonrpc': '2.0', 'id': 2, 'result': {'qReturn': {'qType': 'Doc', 'qHandle': 1, 'qGenericId': 'C:\Users\mohdm\Documents\Qlik\Sense\Apps\Test_2.qvf'}}, 'change': 1}

{'jsonrpc': '2.0', 'id': 3, 'result': {'qReturn': {'qType': 'GenericObject', 'qHandle': 2, 'qGenericType': 'SheetList', 'qGenericId': '4b344780-a350-48db-8b65-27bb5a2c62b2'}}, 'change': 1}

{'jsonrpc': '2.0', 'id': 4, 'result': {'qLayout': {'qInfo': {'qId': '4b344780-a350-48db-8b65-27bb5a2c62b2', 'qType': 'SheetList'}, 'qMeta': {'privileges': ['read', 'update', 'delete', 'exportdata']}, 'qSelectionInfo': {}, 'qAppObjectList': {'qItems': [{'qInfo': {'qId': '8a0f6a01-ef89-4d65-821f-371c26208dcf', 'qType': 'sheet'}, 'qMeta': {'privileges': ['read', 'update', 'delete', 'exportdata'], 'title': 'Sheet_1', 'description': ''}, 'qData': {'rank': None, 'thumbnail': {'qStaticContentUrl': {}}, 'columns': 24, 'rows': 12, 'cells': [{'name': 'kfxNpV', 'type': 'auto-chart', 'col': 0, 'row': 0, 'colspan': 15, 'rowspan': 6, 'bounds': {'y': 0, 'x': 0, 'width': 62.5, 'height': 50}}, {'name': 'qHzmARQ', 'type': 'qlik-barplus-chart', 'col': 0, 'row': 6, 'colspan': 21, 'rowspan': 6, 'bounds': {'y': 50, 'x': 0, 'width': 87.5, 'height': 50}}, {'name': 'BXBQmw', 'type': 'auto-chart', 'col': 15, 'row': 0, 'colspan': 9, 'rowspan': 6, 'bounds': {'y': 0, 'x': 62.5, 'width': 37.5, 'height': 50}}], 'title': 'Sheet_1', 'description': ''}}]}}}}

kfxNpV
qHzmARQ
BXBQmw

I had originally posted this question at Qlik community as well but it didn't get a response.


Solution

  • For this cases I'm usually "observing" Qlik's communication from the browser.

    (in Chrome)

    • open the app
    • open the browser dev tools (press F12)
    • navigate to "Network" (1)
    • navigate to "WS" (2)
    • press the required socket session (3)
    • press "Messages" (4)
    • check whats being send/received in the socket

    (if you cant see the sockets in the Network tab just refresh the page)

    enter image description here

    Below is a Javascript/Node code that is exporting data for one object. (Im hardcoding the object ID in my case)

    const fs = require('fs');
    const axios = require('axios');
    
    const enigma = require('enigma.js');
    const WebSocket = require('ws');
    const schema = require('enigma.js/schemas/12.20.0.json');
    
    const session = enigma.create({
        schema,
        url: 'ws://localhost:9076/app/engineData',
        createSocket: url => new WebSocket(url)
    });
    
    (async function () {
        // open new session
        let global = await session.open();
    
        // open the app
        let doc = await global.openDoc("C:\\Users\\USERNAME\\Documents\\Qlik\\Sense\\Apps\\Consumer_Sales.qvf");
    
        // get the required object
        let qObj = await doc.getObject("MEAjCJ");
    
        // OOXML - export the data in Excel (xlsx) format
        let data = await qObj.exportData("OOXML");
    
        // generate the full download link
        let downloadLink = `http://localhost:4848${data.qUrl}`;
    
        // download and save the file
        await axios.get(downloadLink, { responseType: "stream" })
            .then(response => {
                response.data.pipe(fs.createWriteStream("export.xlsx"));
            });
    })()
    

    Official documentation for the ExportData method is here

    Update - Python code snippet

    The loop through the sheet objects can look like this:

    id = 5
    
    list_of_charts = result_json['result']['qLayout']['qAppObjectList']['qItems'][0]['qData']['cells']
    for chart in list_of_charts:
    
        obj_req = {
            "jsonrpc": "2.0",
            "method": "GetObject",
            "handle": app_req_handle,
            "params": [ chart["name"] ],
            "outKey": -1,
            "id": id
        }
    
        ws.send(json.dumps(obj_req))
        result = ws.recv()
        ws.send(json.dumps(obj_req))
        result = ws.recv()
        result_json = json.loads(result)
        # print(result_json)
    
        obj_req_handle = result_json['result']['qReturn']['qHandle']
    
        export_req = {
            "jsonrpc": "2.0",
            "method": "ExportData",
            "handle": obj_req_handle,
            "params": [ "OOXML" ],
            "outKey": -1,
            "id": 6
        }
    
        ws.send(json.dumps(export_req))
        result = ws.recv()
        ws.send(json.dumps(export_req))
        result = ws.recv()
        result_json = json.loads(result)
    
        downloadURL = "http://localhost:4848" + result_json["result"]["qUrl"]
        r = requests.get(downloadURL, allow_redirects=True)
        open('export_python_' + chart["name"] + '.xlsx', 'wb').write(r.content)
    
        id += 1