Unlock a world of possibilities! Login now and discover the exclusive benefits awaiting you.
Hi,
I am creating a QlikSense dashboard that takes data from a spreadsheet (dates, restaurants, and amounts spent) I created, sends the columns to python, and has python send back the results. It has worked for a YTD summary, but when I try to create an average request, I'm receiving an error message:
ERROR - Exception calling application: 'ExtensionService' object has no attribute '_avg_spent'
Traceback (most recent call last):
File "C:\ProgramData\Anaconda3\lib\site-packages\grpc\_server.py", line 385, in _call_behavior
return behavior(argument, context), True
File "C:/Users/jtotsch/Documents/Qlik/PythonSSE-master/EatingOut/ExtensionService_Adv.py", line 165, in ExecuteFunction
return getattr(self, self.functions[func_id])(request_iterator)
AttributeError: 'ExtensionService' object has no attribute '_avg_spent'
2018-08-22 09:52:08,721 - INFO - ExecuteFunction (functionId: 0)
I've used this same code in another project, so I am not sure what the issue is.
Also, I would like to average the amounts by month and by restaurants, but I am not sure how to get python to send back two columns and how to place those columns into Qlik Sense. Any suggestions would be greatly appreciated.
The code I'm using is below.
Thanks for your assistance!
## Eatingout.xlsx
## New application with sample data
## import packages
import argparse
import json
import logging
import logging.config
import os
import sys
import time
from concurrent import futures
import ServerSideExtension_pb2 as SSE
import grpc
import numpy as np
## Set a day
_ONE_DAY_IN_SECONDS = 60 * 60 *24
## An SSE-plugin
## This one is from the Column Operations example
class ExtensionService(SSE.ConnectorServicer):
## Class initializer
## :param funcdef_file: A function definition JSON file
def __init__(self, funcdef_file):
self._function_definitions = funcdef_file
## Create a logging directory/file
if not os.path.exists('logs'):
os.mkdir('logs')
log_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logger.config')
logging.config.fileConfig(log_file)
logging.info('Logging enabled')
@property
def function_definitions(self):
return self._function_definitions
@property
def functions(self):
return {
0: '_ytd_totals',
1: '_month_totals',
2: '_avg_spent',
3: '_rest_totals'
}
## ytd totals - from spreadsheet data
@staticmethod
def _ytd_totals(request):
params = []
for bundled_rows in request:
for row in bundled_rows.rows:
param = [d.numData for d in row.duals][0]
params.append(param)
## Sum the column, which will be YTD amount
result = sum(params)
## Create an iterable of dual with numerical value
duals = iter([SSE.Dual(numData=result)])
## Yield the row data constructed
yield SSE.BundledRows(rows=[SSE.Row(duals=duals)])
## average spent - from spreadsheet data
@staticmethod
def _avgspent(request):
"""
Summarize the column sent as a parameter. Aggregation function.
:param request: an iterable sequence of RowData
:return: int, sum if column
"""
params = []
# Iterate over bundled rows
for bundled_rows in request:
# Iterating over rows within a bundle of rows
for row in bundled_rows.rows:
# Retrieve numerical value of parameter and append to the params variable
# Length of param is 1 since one column is received, the [0] collects the first value in the list
param = [d.numData for d in row.duals][0]
params.append(param)
# Sum all rows collected the the params variable
result = np.average(params)
# Create an iterable of dual with numerical value
duals = iter([SSE.Dual(numData=result)])
# Yield the row data constructed
yield SSE.BundledRows(rows=[SSE.Row(duals=duals)])
@staticmethod
def _get_function_id(context):
"""
Retrieve function id from header.
:param context: context
:return: function id
"""
metadata = dict(context.invocation_metadata())
header = SSE.FunctionRequestHeader()
header.ParseFromString(metadata['qlik-functionrequestheader-bin'])
return header.functionId
"""
Implementation of rpc functions.
"""
def GetCapabilities(self, request, context):
"""
Get capabilities.
Note that either request or context is used in the implementation of this method, but still added as
parameters. The reason is that gRPC always sends both when making a function call and therefore we must include
them to avoid error messages regarding too many parameters provided from the client.
:param request: the request, not used in this method.
:param context: the context, not used in this method.
:return: the capabilities.
"""
logging.info('GetCapabilities')
# Create an instance of the Capabilities grpc message
# Enable(or disable) script evaluation
# Set values for pluginIdentifier and pluginVersion
capabilities = SSE.Capabilities(allowScript=False,
pluginIdentifier='Advanced Analytics',
pluginVersion='v1.0.0-beta1')
# If user defined functions supported, add the definitions to the message
with open(self.function_definitions) as json_file:
# Iterate over each function definition and add data to the Capabilities grpc message
for definition in json.load(json_file)['Functions']:
function = capabilities.functions.add()
function.name = definition['Name']
function.functionId = definition['Id']
function.functionType = definition['Type']
function.returnType = definition['ReturnType']
# Retrieve name and type of each parameter
for param_name, param_type in sorted(definition['Params'].items()):
function.params.add(name=param_name, dataType=param_type)
logging.info('Adding to capabilities: {}({})'.format(function.name,
[p.name for p in function.params]))
return capabilities
def ExecuteFunction(self, request_iterator, context):
"""
Call corresponding function based on function id sent in header.
:param request_iterator: an iterable sequence of RowData.
:param context: the context.
:return: an iterable sequence of RowData.
"""
# Retrieve function id
func_id = self._get_function_id(context)
logging.info('ExecuteFunction (functionId: {})'.format(func_id))
return getattr(self, self.functions[func_id])(request_iterator)
# """
# Implementation of the Server connecting to gRPC.
# """
def Serve(self, port, pem_dir):
"""
Server
:param port: port to listen on.
:param pem_dir: Directory including certificates
:return: None
"""
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
SSE.add_ConnectorServicer_to_server(self, server)
if pem_dir:
# Secure connection
with open(os.path.join(pem_dir, 'sse_server_key.pem'), 'rb') as f:
private_key = f.read()
with open(os.path.join(pem_dir, 'sse_server_cert.pem'), 'rb') as f:
cert_chain = f.read()
with open(os.path.join(pem_dir, 'root_cert.pem'), 'rb') as f:
root_cert = f.read()
credentials = grpc.ssl_server_credentials([(private_key, cert_chain)], root_cert, True)
server.add_secure_port('[::]:{}'.format(port), credentials)
logging.info('*** Running server in secure mode on port: {} ***'.format(port))
else:
# Insecure connection
server.add_insecure_port('[::]:{}'.format(port))
logging.info('*** Running server in insecure mode on port: {} ***'.format(port))
server.start()
try:
while True:
time.sleep(_ONE_DAY_IN_SECONDS)
except KeyboardInterrupt:
server.stop(0)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--port', nargs='?', default='50053') # The port the plugin is run on
parser.add_argument('--pem_dir', nargs='?') # The directory for authentification certificates
parser.add_argument('--definition-file', nargs='?', default='FuncDefs_advanced.json') # The function defenition file
args = parser.parse_args()
# need to locate the file when script is called from outside it's location dir.
def_file = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), args.definition_file)
calc = ExtensionService(def_file)
calc.Serve(args.port, args.pem_dir)