Skip to main content
cancel
Showing results for 
Search instead for 
Did you mean: 
julietotsch
Contributor II
Contributor II

AttributeError: ExtensionService object has no attribute

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)      

Labels (2)
1 Reply
anne
Partner - Contributor
Partner - Contributor

Are you aware that the method is called _avgspent and not _avg_spent? Seems like a typo in the function naming.