1#!/usr/bin/env python
2
3r"""
4Define the tally_sheet class.
5"""
6
7import sys
8import collections
9import copy
10import re
11
12try:
13    from robot.utils import DotDict
14except ImportError:
15    pass
16
17import gen_print as gp
18
19
20class tally_sheet:
21
22    r"""
23    This class is the implementation of a tally sheet.  The sheet can be
24    viewed as rows and columns.  Each row has a unique key field.
25
26    This class provides methods to tally the results (totals, etc.).
27
28    Example code:
29
30    # Create an ordered dict to represent your field names/initial values.
31    try:
32        boot_results_fields = collections.OrderedDict([('total', 0), ('pass',
33        0), ('fail', 0)])
34    except AttributeError:
35        boot_results_fields = DotDict([('total', 0), ('pass', 0), ('fail', 0)])
36    # Create the tally sheet.
37    boot_test_results = tally_sheet('boot type', boot_results_fields,
38    'boot_test_results')
39    # Set your sum fields (fields which are to be totalled).
40    boot_test_results.set_sum_fields(['total', 'pass', 'fail'])
41    # Set calc fields (within a row, a certain field can be derived from
42    # other fields in the row.
43    boot_test_results.set_calc_fields(['total=pass+fail'])
44
45    # Create some records.
46    boot_test_results.add_row('BMC Power On')
47    boot_test_results.add_row('BMC Power Off')
48
49    # Increment field values.
50    boot_test_results.inc_row_field('BMC Power On', 'pass')
51    boot_test_results.inc_row_field('BMC Power Off', 'pass')
52    boot_test_results.inc_row_field('BMC Power On', 'fail')
53    # Have the results tallied...
54    boot_test_results.calc()
55    # And printed...
56    boot_test_results.print_report()
57
58    Example result:
59
60    Boot Type                           Total Pass Fail
61    ----------------------------------- ----- ---- ----
62    BMC Power On                            2    1    1
63    BMC Power Off                           1    1    0
64    ===================================================
65    Totals                                  3    2    1
66
67    """
68
69    def __init__(self,
70                 row_key_field_name='Description',
71                 init_fields_dict=dict(),
72                 obj_name='tally_sheet'):
73        r"""
74        Create a tally sheet object.
75
76        Description of arguments:
77        row_key_field_name          The name of the row key field (e.g.
78                                    boot_type, team_name, etc.)
79        init_fields_dict            A dictionary which contains field
80                                    names/initial values.
81        obj_name                    The name of the tally sheet.
82        """
83
84        self.__obj_name = obj_name
85        # The row key field uniquely identifies the row.
86        self.__row_key_field_name = row_key_field_name
87        # Create a "table" which is an ordered dictionary.
88        # If we're running python 2.7 or later, collections has an
89        # OrderedDict we can use.  Otherwise, we'll try to use the DotDict (a
90        # robot library).  If neither of those are available, we fail.
91        try:
92            self.__table = collections.OrderedDict()
93        except AttributeError:
94            self.__table = DotDict()
95        # Save the initial fields dictionary.
96        self.__init_fields_dict = init_fields_dict
97        self.__totals_line = init_fields_dict
98        self.__sum_fields = []
99        self.__calc_fields = []
100
101    def init(self,
102             row_key_field_name,
103             init_fields_dict,
104             obj_name='tally_sheet'):
105        self.__init__(row_key_field_name,
106                      init_fields_dict,
107                      obj_name='tally_sheet')
108
109    def set_sum_fields(self, sum_fields):
110        r"""
111        Set the sum fields, i.e. create a list of field names which are to be
112        summed and included on the totals line of reports.
113
114        Description of arguments:
115        sum_fields                  A list of field names.
116        """
117
118        self.__sum_fields = sum_fields
119
120    def set_calc_fields(self, calc_fields):
121        r"""
122        Set the calc fields, i.e. create a list of field names within a given
123        row which are to be calculated for the user.
124
125        Description of arguments:
126        calc_fields                 A string expression such as
127                                    'total=pass+fail' which shows which field
128                                    on a given row is derived from other
129                                    fields in the same row.
130        """
131
132        self.__calc_fields = calc_fields
133
134    def add_row(self, row_key, init_fields_dict=None):
135        r"""
136        Add a row to the tally sheet.
137
138        Description of arguments:
139        row_key                     A unique key value.
140        init_fields_dict            A dictionary of field names/initial
141                                    values.  The number of fields in this
142                                    dictionary must be the same as what was
143                                    specified when the tally sheet was
144                                    created.  If no value is passed, the value
145                                    used to create the tally sheet will be
146                                    used.
147        """
148
149        if init_fields_dict is None:
150            init_fields_dict = self.__init_fields_dict
151        try:
152            self.__table[row_key] = collections.OrderedDict(init_fields_dict)
153        except AttributeError:
154            self.__table[row_key] = DotDict(init_fields_dict)
155
156    def update_row_field(self, row_key, field_key, value):
157        r"""
158        Update a field in a row with the specified value.
159
160        Description of arguments:
161        row_key                     A unique key value that identifies the row
162                                    to be updated.
163        field_key                   The key that identifies which field in the
164                                    row that is to be updated.
165        value                       The value to set into the specified
166                                    row/field.
167        """
168
169        self.__table[row_key][field_key] = value
170
171    def inc_row_field(self, row_key, field_key):
172        r"""
173        Increment the value of the specified field in the specified row.  The
174        value of the field must be numeric.
175
176        Description of arguments:
177        row_key                     A unique key value that identifies the row
178                                    to be updated.
179        field_key                   The key that identifies which field in the
180                                    row that is to be updated.
181        """
182
183        self.__table[row_key][field_key] += 1
184
185    def dec_row_field(self, row_key, field_key):
186        r"""
187        Decrement the value of the specified field in the specified row.  The
188        value of the field must be numeric.
189
190        Description of arguments:
191        row_key                     A unique key value that identifies the row
192                                    to be updated.
193        field_key                   The key that identifies which field in the
194                                    row that is to be updated.
195        """
196
197        self.__table[row_key][field_key] -= 1
198
199    def calc(self):
200        r"""
201        Calculate totals and row calc fields.  Also, return totals_line
202        dictionary.
203        """
204
205        self.__totals_line = copy.deepcopy(self.__init_fields_dict)
206        # Walk through the rows of the table.
207        for row_key, value in self.__table.items():
208            # Walk through the calc fields and process them.
209            for calc_field in self.__calc_fields:
210                tokens = [i for i in re.split(r'(\d+|\W+)', calc_field) if i]
211                cmd_buf = ""
212                for token in tokens:
213                    if token in ("=", "+", "-", "*", "/"):
214                        cmd_buf += token + " "
215                    else:
216                        # Note: Using "mangled" name for the sake of the exec
217                        # statement (below).
218                        cmd_buf += "self._" + self.__class__.__name__ +\
219                                   "__table['" + row_key + "']['" +\
220                                   token + "'] "
221                exec(cmd_buf)
222
223            for field_key, sub_value in value.items():
224                if field_key in self.__sum_fields:
225                    self.__totals_line[field_key] += sub_value
226
227        return self.__totals_line
228
229    def sprint_obj(self):
230        r"""
231        sprint the fields of this object.  This would normally be for debug
232        purposes.
233        """
234
235        buffer = ""
236
237        buffer += "class name: " + self.__class__.__name__ + "\n"
238        buffer += gp.sprint_var(self.__obj_name)
239        buffer += gp.sprint_var(self.__row_key_field_name)
240        buffer += gp.sprint_var(self.__table)
241        buffer += gp.sprint_var(self.__init_fields_dict)
242        buffer += gp.sprint_var(self.__sum_fields)
243        buffer += gp.sprint_var(self.__totals_line)
244        buffer += gp.sprint_var(self.__calc_fields)
245        buffer += gp.sprint_var(self.__table)
246
247        return buffer
248
249    def print_obj(self):
250        r"""
251        print the fields of this object to stdout.  This would normally be for
252        debug purposes.
253        """
254
255        sys.stdout.write(self.sprint_obj())
256
257    def sprint_report(self):
258        r"""
259        sprint the tally sheet in a formatted way.
260        """
261
262        buffer = ""
263        # Build format strings.
264        col_names = [self.__row_key_field_name.title()]
265        report_width = 40
266        key_width = 40
267        format_string = '{0:<' + str(key_width) + '}'
268        dash_format_string = '{0:-<' + str(key_width) + '}'
269        field_num = 0
270
271        first_rec = next(iter(self.__table.items()))
272        for row_key, value in first_rec[1].items():
273            field_num += 1
274            if isinstance(value, int):
275                align = ':>'
276            else:
277                align = ':<'
278            format_string += ' {' + str(field_num) + align +\
279                             str(len(row_key)) + '}'
280            dash_format_string += ' {' + str(field_num) + ':->' +\
281                                  str(len(row_key)) + '}'
282            report_width += 1 + len(row_key)
283            col_names.append(row_key.title())
284        num_fields = field_num + 1
285        totals_line_fmt = '{0:=<' + str(report_width) + '}'
286
287        buffer += format_string.format(*col_names) + "\n"
288        buffer += dash_format_string.format(*([''] * num_fields)) + "\n"
289        for row_key, value in self.__table.items():
290            buffer += format_string.format(row_key, *value.values()) + "\n"
291
292        buffer += totals_line_fmt.format('') + "\n"
293        buffer += format_string.format('Totals',
294                                       *self.__totals_line.values()) + "\n"
295
296        return buffer
297
298    def print_report(self):
299        r"""
300        print the tally sheet in a formatted way.
301        """
302
303        sys.stdout.write(self.sprint_report())
304