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.  The sheet can be viewed as rows and columns.  Each
23    row has a unique key field.
24
25    This class provides methods to tally the results (totals, etc.).
26
27    Example code:
28
29    # Create an ordered dict to represent your field names/initial values.
30    try:
31        boot_results_fields = collections.OrderedDict([('total', 0), ('pass', 0), ('fail', 0)])
32    except AttributeError:
33        boot_results_fields = DotDict([('total', 0), ('pass', 0), ('fail', 0)])
34    # Create the tally sheet.
35    boot_test_results = tally_sheet('boot type', boot_results_fields, 'boot_test_results')
36    # Set your sum fields (fields which are to be totalled).
37    boot_test_results.set_sum_fields(['total', 'pass', 'fail'])
38    # Set calc fields (within a row, a certain field can be derived from other fields in the row.
39    boot_test_results.set_calc_fields(['total=pass+fail'])
40
41    # Create some records.
42    boot_test_results.add_row('BMC Power On')
43    boot_test_results.add_row('BMC Power Off')
44
45    # Increment field values.
46    boot_test_results.inc_row_field('BMC Power On', 'pass')
47    boot_test_results.inc_row_field('BMC Power Off', 'pass')
48    boot_test_results.inc_row_field('BMC Power On', 'fail')
49    # Have the results tallied...
50    boot_test_results.calc()
51    # And printed...
52    boot_test_results.print_report()
53
54    Example result:
55
56    Boot Type                           Total Pass Fail
57    ----------------------------------- ----- ---- ----
58    BMC Power On                            2    1    1
59    BMC Power Off                           1    1    0
60    ===================================================
61    Totals                          3    2    1
62
63    """
64
65    def __init__(
66        self,
67        row_key_field_name="Description",
68        init_fields_dict=dict(),
69        obj_name="tally_sheet",
70    ):
71        r"""
72        Create a tally sheet object.
73
74        Description of arguments:
75        row_key_field_name          The name of the row key field (e.g. boot_type, team_name, etc.)
76        init_fields_dict            A dictionary which contains field names/initial values.
77        obj_name                    The name of the tally sheet.
78        """
79
80        self.__obj_name = obj_name
81        # The row key field uniquely identifies the row.
82        self.__row_key_field_name = row_key_field_name
83        # Create a "table" which is an ordered dictionary.
84        # If we're running python 2.7 or later, collections has an OrderedDict we can use.  Otherwise, we'll
85        # try to use the DotDict (a robot library).  If neither of those are available, we fail.
86        try:
87            self.__table = collections.OrderedDict()
88        except AttributeError:
89            self.__table = DotDict()
90        # Save the initial fields dictionary.
91        self.__init_fields_dict = init_fields_dict
92        self.__totals_line = init_fields_dict
93        self.__sum_fields = []
94        self.__calc_fields = []
95
96    def init(
97        self, row_key_field_name, init_fields_dict, obj_name="tally_sheet"
98    ):
99        self.__init__(
100            row_key_field_name, init_fields_dict, obj_name="tally_sheet"
101        )
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 += (
205                            "self._"
206                            + self.__class__.__name__
207                            + "__table['"
208                            + row_key
209                            + "']['"
210                            + token
211                            + "'] "
212                        )
213                exec(cmd_buf)
214
215            for field_key, sub_value in value.items():
216                if field_key in self.__sum_fields:
217                    self.__totals_line[field_key] += sub_value
218
219        return self.__totals_line
220
221    def sprint_obj(self):
222        r"""
223        sprint the fields of this object.  This would normally be for debug purposes.
224        """
225
226        buffer = ""
227
228        buffer += "class name: " + self.__class__.__name__ + "\n"
229        buffer += gp.sprint_var(self.__obj_name)
230        buffer += gp.sprint_var(self.__row_key_field_name)
231        buffer += gp.sprint_var(self.__table)
232        buffer += gp.sprint_var(self.__init_fields_dict)
233        buffer += gp.sprint_var(self.__sum_fields)
234        buffer += gp.sprint_var(self.__totals_line)
235        buffer += gp.sprint_var(self.__calc_fields)
236        buffer += gp.sprint_var(self.__table)
237
238        return buffer
239
240    def print_obj(self):
241        r"""
242        print the fields of this object to stdout.  This would normally be for debug purposes.
243        """
244
245        sys.stdout.write(self.sprint_obj())
246
247    def sprint_report(self):
248        r"""
249        sprint the tally sheet in a formatted way.
250        """
251
252        buffer = ""
253        # Build format strings.
254        col_names = [self.__row_key_field_name.title()]
255        report_width = 40
256        key_width = 40
257        format_string = "{0:<" + str(key_width) + "}"
258        dash_format_string = "{0:-<" + str(key_width) + "}"
259        field_num = 0
260
261        try:
262            first_rec = next(iter(self.__table.items()))
263            for row_key, value in first_rec[1].items():
264                field_num += 1
265                if isinstance(value, int):
266                    align = ":>"
267                else:
268                    align = ":<"
269                format_string += (
270                    " {" + str(field_num) + align + str(len(row_key)) + "}"
271                )
272                dash_format_string += (
273                    " {" + str(field_num) + ":->" + str(len(row_key)) + "}"
274                )
275                report_width += 1 + len(row_key)
276                col_names.append(row_key.title())
277        except StopIteration:
278            pass
279        num_fields = field_num + 1
280        totals_line_fmt = "{0:=<" + str(report_width) + "}"
281
282        buffer += format_string.format(*col_names) + "\n"
283        buffer += dash_format_string.format(*([""] * num_fields)) + "\n"
284        for row_key, value in self.__table.items():
285            buffer += format_string.format(row_key, *value.values()) + "\n"
286
287        buffer += totals_line_fmt.format("") + "\n"
288        buffer += (
289            format_string.format("Totals", *self.__totals_line.values()) + "\n"
290        )
291
292        return buffer
293
294    def print_report(self):
295        r"""
296        print the tally sheet in a formatted way.
297        """
298
299        sys.stdout.write(self.sprint_report())
300