blob: e3280c3a9a519aad2b3c482fe6e319752951ee35 [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
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###############################################################################