blob: d91d4c49361ffb237b76784d1adf6c141d9c00e8 [file] [log] [blame]
Michael Walshfefcbca2016-11-11 14:28:34 -06001#!/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
Michael Walshfefcbca2016-11-11 14:28:34 -060020class 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())