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