blob: 322fac60f74e11484024205edc95f228c14b8c35 [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
Michael Walsh86f029f2018-03-27 11:58:30 -050060 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
Michael Walshfefcbca2016-11-11 14:28:34 -060066
67 """
68
69 def __init__(self,
70 row_key_field_name='Description',
71 init_fields_dict=dict(),
72 obj_name='tally_sheet'):
Michael Walshfefcbca2016-11-11 14:28:34 -060073 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):
Michael Walshfefcbca2016-11-11 14:28:34 -0600110 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):
Michael Walshfefcbca2016-11-11 14:28:34 -0600121 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):
Michael Walshfefcbca2016-11-11 14:28:34 -0600135 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
Michael Walsh5f4bce82019-07-16 16:33:01 -0500149 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)
Michael Walshfefcbca2016-11-11 14:28:34 -0600154 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):
Michael Walshfefcbca2016-11-11 14:28:34 -0600162 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):
Michael Walshfefcbca2016-11-11 14:28:34 -0600177 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):
Michael Walshfefcbca2016-11-11 14:28:34 -0600191 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):
Michael Walshfefcbca2016-11-11 14:28:34 -0600205 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):
Michael Walshfefcbca2016-11-11 14:28:34 -0600235 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):
Michael Walshfefcbca2016-11-11 14:28:34 -0600255 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):
Michael Walshfefcbca2016-11-11 14:28:34 -0600263 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()]
Michael Walsh86f029f2018-03-27 11:58:30 -0500270 report_width = 40
271 key_width = 40
Michael Walshfefcbca2016-11-11 14:28:34 -0600272 format_string = '{0:<' + str(key_width) + '}'
273 dash_format_string = '{0:-<' + str(key_width) + '}'
274 field_num = 0
275
Michael Walsh5f4bce82019-07-16 16:33:01 -0500276 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
Michael Walshfefcbca2016-11-11 14:28:34 -0600292 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):
Michael Walshfefcbca2016-11-11 14:28:34 -0600307 r"""
308 print the tally sheet in a formatted way.
309 """
310
311 sys.stdout.write(self.sprint_report())