1#!/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 20class 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 60 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 66 67 """ 68 69 def __init__(self, 70 row_key_field_name='Description', 71 init_fields_dict=dict(), 72 obj_name='tally_sheet'): 73 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): 110 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): 121 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): 135 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 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 162 to be updated. 163 field_key The key that identifies which field in the 164 row that is to be updated. 165 value The value to set into the specified 166 row/field. 167 """ 168 169 self.__table[row_key][field_key] = value 170 171 def inc_row_field(self, row_key, field_key): 172 r""" 173 Increment the value of the specified field in the specified row. The 174 value of the field must be numeric. 175 176 Description of arguments: 177 row_key A unique key value that identifies the row 178 to be updated. 179 field_key The key that identifies which field in the 180 row that is to be updated. 181 """ 182 183 self.__table[row_key][field_key] += 1 184 185 def dec_row_field(self, row_key, field_key): 186 r""" 187 Decrement the value of the specified field in the specified row. The 188 value of the field must be numeric. 189 190 Description of arguments: 191 row_key A unique key value that identifies the row 192 to be updated. 193 field_key The key that identifies which field in the 194 row 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 202 dictionary. 203 """ 204 205 self.__totals_line = copy.deepcopy(self.__init_fields_dict) 206 # Walk through the rows of the table. 207 for row_key, value in self.__table.items(): 208 # Walk through the calc fields and process them. 209 for calc_field in self.__calc_fields: 210 tokens = [i for i in re.split(r'(\d+|\W+)', calc_field) if i] 211 cmd_buf = "" 212 for token in tokens: 213 if token in ("=", "+", "-", "*", "/"): 214 cmd_buf += token + " " 215 else: 216 # Note: Using "mangled" name for the sake of the exec 217 # statement (below). 218 cmd_buf += "self._" + self.__class__.__name__ +\ 219 "__table['" + row_key + "']['" +\ 220 token + "'] " 221 exec(cmd_buf) 222 223 for field_key, sub_value in value.items(): 224 if field_key in self.__sum_fields: 225 self.__totals_line[field_key] += sub_value 226 227 return self.__totals_line 228 229 def sprint_obj(self): 230 r""" 231 sprint the fields of this object. This would normally be for debug 232 purposes. 233 """ 234 235 buffer = "" 236 237 buffer += "class name: " + self.__class__.__name__ + "\n" 238 buffer += gp.sprint_var(self.__obj_name) 239 buffer += gp.sprint_var(self.__row_key_field_name) 240 buffer += gp.sprint_var(self.__table) 241 buffer += gp.sprint_var(self.__init_fields_dict) 242 buffer += gp.sprint_var(self.__sum_fields) 243 buffer += gp.sprint_var(self.__totals_line) 244 buffer += gp.sprint_var(self.__calc_fields) 245 buffer += gp.sprint_var(self.__table) 246 247 return buffer 248 249 def print_obj(self): 250 r""" 251 print the fields of this object to stdout. This would normally be for 252 debug purposes. 253 """ 254 255 sys.stdout.write(self.sprint_obj()) 256 257 def sprint_report(self): 258 r""" 259 sprint the tally sheet in a formatted way. 260 """ 261 262 buffer = "" 263 # Build format strings. 264 col_names = [self.__row_key_field_name.title()] 265 report_width = 40 266 key_width = 40 267 format_string = '{0:<' + str(key_width) + '}' 268 dash_format_string = '{0:-<' + str(key_width) + '}' 269 field_num = 0 270 271 first_rec = next(iter(self.__table.items())) 272 for row_key, value in first_rec[1].items(): 273 field_num += 1 274 if type(value) is int: 275 align = ':>' 276 else: 277 align = ':<' 278 format_string += ' {' + str(field_num) + align +\ 279 str(len(row_key)) + '}' 280 dash_format_string += ' {' + str(field_num) + ':->' +\ 281 str(len(row_key)) + '}' 282 report_width += 1 + len(row_key) 283 col_names.append(row_key.title()) 284 num_fields = field_num + 1 285 totals_line_fmt = '{0:=<' + str(report_width) + '}' 286 287 buffer += format_string.format(*col_names) + "\n" 288 buffer += dash_format_string.format(*([''] * num_fields)) + "\n" 289 for row_key, value in self.__table.items(): 290 buffer += format_string.format(row_key, *value.values()) + "\n" 291 292 buffer += totals_line_fmt.format('') + "\n" 293 buffer += format_string.format('Totals', 294 *self.__totals_line.values()) + "\n" 295 296 return buffer 297 298 def print_report(self): 299 r""" 300 print the tally sheet in a formatted way. 301 """ 302 303 sys.stdout.write(self.sprint_report()) 304