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