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