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