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