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 row_key in self.__table:
150            # If we allow this, the row values get re-initialized.
151            message = "An entry for \"" + row_key + "\" already exists in"
152            message += " tally sheet."
153            raise ValueError(message)
154        if init_fields_dict is None:
155            init_fields_dict = self.__init_fields_dict
156        try:
157            self.__table[row_key] = collections.OrderedDict(init_fields_dict)
158        except AttributeError:
159            self.__table[row_key] = DotDict(init_fields_dict)
160
161    def update_row_field(self, row_key, field_key, value):
162        r"""
163        Update a field in a row with the specified value.
164
165        Description of arguments:
166        row_key                     A unique key value that identifies the row
167                                    to be updated.
168        field_key                   The key that identifies which field in the
169                                    row that is to be updated.
170        value                       The value to set into the specified
171                                    row/field.
172        """
173
174        self.__table[row_key][field_key] = value
175
176    def inc_row_field(self, row_key, field_key):
177        r"""
178        Increment the value of the specified field in the specified row.  The
179        value of the field must be numeric.
180
181        Description of arguments:
182        row_key                     A unique key value that identifies the row
183                                    to be updated.
184        field_key                   The key that identifies which field in the
185                                    row that is to be updated.
186        """
187
188        self.__table[row_key][field_key] += 1
189
190    def dec_row_field(self, row_key, field_key):
191        r"""
192        Decrement the value of the specified field in the specified row.  The
193        value of the field must be numeric.
194
195        Description of arguments:
196        row_key                     A unique key value that identifies the row
197                                    to be updated.
198        field_key                   The key that identifies which field in the
199                                    row that is to be updated.
200        """
201
202        self.__table[row_key][field_key] -= 1
203
204    def calc(self):
205        r"""
206        Calculate totals and row calc fields.  Also, return totals_line
207        dictionary.
208        """
209
210        self.__totals_line = copy.deepcopy(self.__init_fields_dict)
211        # Walk through the rows of the table.
212        for row_key, value in self.__table.items():
213            # Walk through the calc fields and process them.
214            for calc_field in self.__calc_fields:
215                tokens = [i for i in re.split(r'(\d+|\W+)', calc_field) if i]
216                cmd_buf = ""
217                for token in tokens:
218                    if token in ("=", "+", "-", "*", "/"):
219                        cmd_buf += token + " "
220                    else:
221                        # Note: Using "mangled" name for the sake of the exec
222                        # statement (below).
223                        cmd_buf += "self._" + self.__class__.__name__ +\
224                                   "__table['" + row_key + "']['" +\
225                                   token + "'] "
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
237        purposes.
238        """
239
240        buffer = ""
241
242        buffer += "class name: " + self.__class__.__name__ + "\n"
243        buffer += gp.sprint_var(self.__obj_name)
244        buffer += gp.sprint_var(self.__row_key_field_name)
245        buffer += gp.sprint_var(self.__table)
246        buffer += gp.sprint_var(self.__init_fields_dict)
247        buffer += gp.sprint_var(self.__sum_fields)
248        buffer += gp.sprint_var(self.__totals_line)
249        buffer += gp.sprint_var(self.__calc_fields)
250        buffer += gp.sprint_var(self.__table)
251
252        return buffer
253
254    def print_obj(self):
255        r"""
256        print the fields of this object to stdout.  This would normally be for
257        debug purposes.
258        """
259
260        sys.stdout.write(self.sprint_obj())
261
262    def sprint_report(self):
263        r"""
264        sprint the tally sheet in a formatted way.
265        """
266
267        buffer = ""
268        # Build format strings.
269        col_names = [self.__row_key_field_name.title()]
270        report_width = 40
271        key_width = 40
272        format_string = '{0:<' + str(key_width) + '}'
273        dash_format_string = '{0:-<' + str(key_width) + '}'
274        field_num = 0
275
276        try:
277            first_rec = next(iter(self.__table.items()))
278            for row_key, value in first_rec[1].items():
279                field_num += 1
280                if isinstance(value, int):
281                    align = ':>'
282                else:
283                    align = ':<'
284                format_string += ' {' + str(field_num) + align +\
285                                 str(len(row_key)) + '}'
286                dash_format_string += ' {' + str(field_num) + ':->' +\
287                                      str(len(row_key)) + '}'
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 += format_string.format('Totals',
302                                       *self.__totals_line.values()) + "\n"
303
304        return buffer
305
306    def print_report(self):
307        r"""
308        print the tally sheet in a formatted way.
309        """
310
311        sys.stdout.write(self.sprint_report())
312