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. The sheet can be viewed as rows and columns. Each 23 row has a unique key field. 24 25 This class provides methods to tally the results (totals, etc.). 26 27 Example code: 28 29 # Create an ordered dict to represent your field names/initial values. 30 try: 31 boot_results_fields = collections.OrderedDict([('total', 0), ('pass', 0), ('fail', 0)]) 32 except AttributeError: 33 boot_results_fields = DotDict([('total', 0), ('pass', 0), ('fail', 0)]) 34 # Create the tally sheet. 35 boot_test_results = tally_sheet('boot type', boot_results_fields, 'boot_test_results') 36 # Set your sum fields (fields which are to be totalled). 37 boot_test_results.set_sum_fields(['total', 'pass', 'fail']) 38 # Set calc fields (within a row, a certain field can be derived from other fields in the row. 39 boot_test_results.set_calc_fields(['total=pass+fail']) 40 41 # Create some records. 42 boot_test_results.add_row('BMC Power On') 43 boot_test_results.add_row('BMC Power Off') 44 45 # Increment field values. 46 boot_test_results.inc_row_field('BMC Power On', 'pass') 47 boot_test_results.inc_row_field('BMC Power Off', 'pass') 48 boot_test_results.inc_row_field('BMC Power On', 'fail') 49 # Have the results tallied... 50 boot_test_results.calc() 51 # And printed... 52 boot_test_results.print_report() 53 54 Example result: 55 56 Boot Type Total Pass Fail 57 ----------------------------------- ----- ---- ---- 58 BMC Power On 2 1 1 59 BMC Power Off 1 1 0 60 =================================================== 61 Totals 3 2 1 62 63 """ 64 65 def __init__( 66 self, 67 row_key_field_name="Description", 68 init_fields_dict=dict(), 69 obj_name="tally_sheet", 70 ): 71 r""" 72 Create a tally sheet object. 73 74 Description of arguments: 75 row_key_field_name The name of the row key field (e.g. boot_type, team_name, etc.) 76 init_fields_dict A dictionary which contains field names/initial values. 77 obj_name The name of the tally sheet. 78 """ 79 80 self.__obj_name = obj_name 81 # The row key field uniquely identifies the row. 82 self.__row_key_field_name = row_key_field_name 83 # Create a "table" which is an ordered dictionary. 84 # If we're running python 2.7 or later, collections has an OrderedDict we can use. Otherwise, we'll 85 # try to use the DotDict (a robot library). If neither of those are available, we fail. 86 try: 87 self.__table = collections.OrderedDict() 88 except AttributeError: 89 self.__table = DotDict() 90 # Save the initial fields dictionary. 91 self.__init_fields_dict = init_fields_dict 92 self.__totals_line = init_fields_dict 93 self.__sum_fields = [] 94 self.__calc_fields = [] 95 96 def init( 97 self, row_key_field_name, init_fields_dict, obj_name="tally_sheet" 98 ): 99 self.__init__( 100 row_key_field_name, init_fields_dict, obj_name="tally_sheet" 101 ) 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 += ( 205 "self._" 206 + self.__class__.__name__ 207 + "__table['" 208 + row_key 209 + "']['" 210 + token 211 + "'] " 212 ) 213 exec(cmd_buf) 214 215 for field_key, sub_value in value.items(): 216 if field_key in self.__sum_fields: 217 self.__totals_line[field_key] += sub_value 218 219 return self.__totals_line 220 221 def sprint_obj(self): 222 r""" 223 sprint the fields of this object. This would normally be for debug purposes. 224 """ 225 226 buffer = "" 227 228 buffer += "class name: " + self.__class__.__name__ + "\n" 229 buffer += gp.sprint_var(self.__obj_name) 230 buffer += gp.sprint_var(self.__row_key_field_name) 231 buffer += gp.sprint_var(self.__table) 232 buffer += gp.sprint_var(self.__init_fields_dict) 233 buffer += gp.sprint_var(self.__sum_fields) 234 buffer += gp.sprint_var(self.__totals_line) 235 buffer += gp.sprint_var(self.__calc_fields) 236 buffer += gp.sprint_var(self.__table) 237 238 return buffer 239 240 def print_obj(self): 241 r""" 242 print the fields of this object to stdout. This would normally be for debug purposes. 243 """ 244 245 sys.stdout.write(self.sprint_obj()) 246 247 def sprint_report(self): 248 r""" 249 sprint the tally sheet in a formatted way. 250 """ 251 252 buffer = "" 253 # Build format strings. 254 col_names = [self.__row_key_field_name.title()] 255 report_width = 40 256 key_width = 40 257 format_string = "{0:<" + str(key_width) + "}" 258 dash_format_string = "{0:-<" + str(key_width) + "}" 259 field_num = 0 260 261 try: 262 first_rec = next(iter(self.__table.items())) 263 for row_key, value in first_rec[1].items(): 264 field_num += 1 265 if isinstance(value, int): 266 align = ":>" 267 else: 268 align = ":<" 269 format_string += ( 270 " {" + str(field_num) + align + str(len(row_key)) + "}" 271 ) 272 dash_format_string += ( 273 " {" + str(field_num) + ":->" + str(len(row_key)) + "}" 274 ) 275 report_width += 1 + len(row_key) 276 col_names.append(row_key.title()) 277 except StopIteration: 278 pass 279 num_fields = field_num + 1 280 totals_line_fmt = "{0:=<" + str(report_width) + "}" 281 282 buffer += format_string.format(*col_names) + "\n" 283 buffer += dash_format_string.format(*([""] * num_fields)) + "\n" 284 for row_key, value in self.__table.items(): 285 buffer += format_string.format(row_key, *value.values()) + "\n" 286 287 buffer += totals_line_fmt.format("") + "\n" 288 buffer += ( 289 format_string.format("Totals", *self.__totals_line.values()) + "\n" 290 ) 291 292 return buffer 293 294 def print_report(self): 295 r""" 296 print the tally sheet in a formatted way. 297 """ 298 299 sys.stdout.write(self.sprint_report()) 300