1#!/usr/bin/env python3
2
3r"""
4Define the tally_sheet class.
5"""
6
7import collections
8import copy
9import re
10import sys
11
12try:
13    from robot.utils import DotDict
14except ImportError:
15    pass
16
17import gen_print as gp
18
19
20class tally_sheet:
21    r"""
22    This class is the implementation of a tally sheet.
23    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__(
67        self,
68        row_key_field_name="Description",
69        init_fields_dict=dict(),
70        obj_name="tally_sheet",
71    ):
72        r"""
73        Create a tally sheet object.
74
75        Description of arguments:
76        row_key_field_name          The name of the row key field (e.g. boot_type, team_name, etc.)
77        init_fields_dict            A dictionary which contains field names/initial values.
78        obj_name                    The name of the tally sheet.
79        """
80
81        self.__obj_name = obj_name
82        # The row key field uniquely identifies the row.
83        self.__row_key_field_name = row_key_field_name
84        # Create a "table" which is an ordered dictionary.
85        # If we're running python 2.7 or later, collections has an OrderedDict
86        # we can use.  Otherwise, we'll try to use the DotDict (a robot library).
87        # If neither of those are available, we fail.
88        try:
89            self.__table = collections.OrderedDict()
90        except AttributeError:
91            self.__table = DotDict()
92        # Save the initial fields dictionary.
93        self.__init_fields_dict = init_fields_dict
94        self.__totals_line = init_fields_dict
95        self.__sum_fields = []
96        self.__calc_fields = []
97
98    def init(
99        self, row_key_field_name, init_fields_dict, obj_name="tally_sheet"
100    ):
101        self.__init__(
102            row_key_field_name, init_fields_dict, obj_name="tally_sheet"
103        )
104
105    def set_sum_fields(self, sum_fields):
106        r"""
107        Set the sum fields, i.e. create a list of field names which are to be
108        summed and included on the totals line of reports.
109
110        Description of arguments:
111        sum_fields                  A list of field names.
112        """
113
114        self.__sum_fields = sum_fields
115
116    def set_calc_fields(self, calc_fields):
117        r"""
118        Set the calc fields, i.e. create a list of field names within a given
119        row which are to be calculated
120        for the user.
121
122        Description of arguments:
123        calc_fields             A string expression such as 'total=pass+fail'
124                                which shows which field on a given row is
125                                derived from other fields in the same row.
126        """
127
128        self.__calc_fields = calc_fields
129
130    def add_row(self, row_key, init_fields_dict=None):
131        r"""
132        Add a row to the tally sheet.
133
134        Description of arguments:
135        row_key                 A unique key value.
136        init_fields_dict        A dictionary of field names/initial values.
137                                The number of fields in this dictionary must
138                                be the same as what was specified when the
139                                tally sheet was created. If no value is passed,
140                                the value used to create the tally sheet will
141                                be used.
142        """
143
144        if row_key in self.__table:
145            # If we allow this, the row values get re-initialized.
146            message = 'An entry for "' + row_key + '" already exists in'
147            message += " tally sheet."
148            raise ValueError(message)
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 to
162                                be updated.
163        field_key               The key that identifies which field in the row
164                                that is to be updated.
165        value                   The value to set into the specified row/field.
166        """
167
168        self.__table[row_key][field_key] = value
169
170    def inc_row_field(self, row_key, field_key):
171        r"""
172        Increment the value of the specified field in the specified row.
173        The value of the field must be numeric.
174
175        Description of arguments:
176        row_key                 A unique key value that identifies the row to
177                                be updated.
178        field_key               The key that identifies which field in the row
179                                that is to be updated.
180        """
181
182        self.__table[row_key][field_key] += 1
183
184    def dec_row_field(self, row_key, field_key):
185        r"""
186        Decrement the value of the specified field in the specified row.
187        The value of the field must be
188        numeric.
189
190        Description of arguments:
191        row_key                 A unique key value that identifies the row to
192                                be updated.
193        field_key               The key that identifies which field in the row
194                                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 dictionary.
202        """
203
204        self.__totals_line = copy.deepcopy(self.__init_fields_dict)
205        # Walk through the rows of the table.
206        for row_key, value in self.__table.items():
207            # Walk through the calc fields and process them.
208            for calc_field in self.__calc_fields:
209                tokens = [i for i in re.split(r"(\d+|\W+)", calc_field) if i]
210                cmd_buf = ""
211                for token in tokens:
212                    if token in ("=", "+", "-", "*", "/"):
213                        cmd_buf += token + " "
214                    else:
215                        # Note: Using "mangled" name for the sake of the exec
216                        # statement (below).
217                        cmd_buf += (
218                            "self._"
219                            + self.__class__.__name__
220                            + "__table['"
221                            + row_key
222                            + "']['"
223                            + token
224                            + "'] "
225                        )
226                exec(cmd_buf)
227
228            for field_key, sub_value in value.items():
229                if field_key in self.__sum_fields:
230                    self.__totals_line[field_key] += sub_value
231
232        return self.__totals_line
233
234    def sprint_obj(self):
235        r"""
236        sprint the fields of this object.  This would normally be for debug purposes.
237        """
238
239        buffer = ""
240
241        buffer += "class name: " + self.__class__.__name__ + "\n"
242        buffer += gp.sprint_var(self.__obj_name)
243        buffer += gp.sprint_var(self.__row_key_field_name)
244        buffer += gp.sprint_var(self.__table)
245        buffer += gp.sprint_var(self.__init_fields_dict)
246        buffer += gp.sprint_var(self.__sum_fields)
247        buffer += gp.sprint_var(self.__totals_line)
248        buffer += gp.sprint_var(self.__calc_fields)
249        buffer += gp.sprint_var(self.__table)
250
251        return buffer
252
253    def print_obj(self):
254        r"""
255        print the fields of this object to stdout.  This would normally be for debug purposes.
256        """
257
258        sys.stdout.write(self.sprint_obj())
259
260    def sprint_report(self):
261        r"""
262        sprint the tally sheet in a formatted way.
263        """
264
265        buffer = ""
266        # Build format strings.
267        col_names = [self.__row_key_field_name.title()]
268        report_width = 40
269        key_width = 40
270        format_string = "{0:<" + str(key_width) + "}"
271        dash_format_string = "{0:-<" + str(key_width) + "}"
272        field_num = 0
273
274        try:
275            first_rec = next(iter(self.__table.items()))
276            for row_key, value in first_rec[1].items():
277                field_num += 1
278                if isinstance(value, int):
279                    align = ":>"
280                else:
281                    align = ":<"
282                format_string += (
283                    " {" + str(field_num) + align + str(len(row_key)) + "}"
284                )
285                dash_format_string += (
286                    " {" + str(field_num) + ":->" + str(len(row_key)) + "}"
287                )
288                report_width += 1 + len(row_key)
289                col_names.append(row_key.title())
290        except StopIteration:
291            pass
292        num_fields = field_num + 1
293        totals_line_fmt = "{0:=<" + str(report_width) + "}"
294
295        buffer += format_string.format(*col_names) + "\n"
296        buffer += dash_format_string.format(*([""] * num_fields)) + "\n"
297        for row_key, value in self.__table.items():
298            buffer += format_string.format(row_key, *value.values()) + "\n"
299
300        buffer += totals_line_fmt.format("") + "\n"
301        buffer += (
302            format_string.format("Totals", *self.__totals_line.values()) + "\n"
303        )
304
305        return buffer
306
307    def print_report(self):
308        r"""
309        print the tally sheet in a formatted way.
310        """
311
312        sys.stdout.write(self.sprint_report())
313