| George Keishing | e7e9171 | 2021-09-03 11:28:44 -0500 | [diff] [blame] | 1 | #!/usr/bin/env python3 | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 2 |  | 
|  | 3 | r""" | 
|  | 4 | Define the tally_sheet class. | 
|  | 5 | """ | 
|  | 6 |  | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 7 | import collections | 
|  | 8 | import copy | 
|  | 9 | import re | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 10 | import sys | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 11 |  | 
|  | 12 | try: | 
|  | 13 | from robot.utils import DotDict | 
|  | 14 | except ImportError: | 
|  | 15 | pass | 
|  | 16 |  | 
|  | 17 | import gen_print as gp | 
|  | 18 |  | 
|  | 19 |  | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 20 | class tally_sheet: | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 21 | r""" | 
| George Keishing | 3996704 | 2023-02-16 00:43:51 -0600 | [diff] [blame] | 22 | This class is the implementation of a tally sheet. | 
|  | 23 | The sheet can be viewed as rows and columns.  Each | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 24 | row has a unique key field. | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 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: | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 32 | boot_results_fields = collections.OrderedDict([('total', 0), ('pass', 0), ('fail', 0)]) | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 33 | except AttributeError: | 
|  | 34 | boot_results_fields = DotDict([('total', 0), ('pass', 0), ('fail', 0)]) | 
|  | 35 | # Create the tally sheet. | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 36 | boot_test_results = tally_sheet('boot type', boot_results_fields, 'boot_test_results') | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 37 | # Set your sum fields (fields which are to be totalled). | 
|  | 38 | boot_test_results.set_sum_fields(['total', 'pass', 'fail']) | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 39 | # Set calc fields (within a row, a certain field can be derived from other fields in the row. | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 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 |  | 
| Michael Walsh | 86f029f | 2018-03-27 11:58:30 -0500 | [diff] [blame] | 57 | Boot Type                           Total Pass Fail | 
|  | 58 | ----------------------------------- ----- ---- ---- | 
|  | 59 | BMC Power On                            2    1    1 | 
|  | 60 | BMC Power Off                           1    1    0 | 
|  | 61 | =================================================== | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 62 | Totals                          3    2    1 | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 63 |  | 
|  | 64 | """ | 
|  | 65 |  | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 66 | def __init__( | 
|  | 67 | self, | 
|  | 68 | row_key_field_name="Description", | 
|  | 69 | init_fields_dict=dict(), | 
|  | 70 | obj_name="tally_sheet", | 
|  | 71 | ): | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 72 | r""" | 
|  | 73 | Create a tally sheet object. | 
|  | 74 |  | 
|  | 75 | Description of arguments: | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 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. | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 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. | 
| George Keishing | 3996704 | 2023-02-16 00:43:51 -0600 | [diff] [blame] | 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. | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 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 |  | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 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 | ) | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 104 |  | 
|  | 105 | def set_sum_fields(self, sum_fields): | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 106 | r""" | 
| George Keishing | 3996704 | 2023-02-16 00:43:51 -0600 | [diff] [blame] | 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. | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 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): | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 117 | r""" | 
| George Keishing | 3996704 | 2023-02-16 00:43:51 -0600 | [diff] [blame] | 118 | Set the calc fields, i.e. create a list of field names within a given | 
|  | 119 | row which are to be calculated | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 120 | for the user. | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 121 |  | 
|  | 122 | Description of arguments: | 
| George Keishing | 3996704 | 2023-02-16 00:43:51 -0600 | [diff] [blame] | 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. | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 126 | """ | 
|  | 127 |  | 
|  | 128 | self.__calc_fields = calc_fields | 
|  | 129 |  | 
|  | 130 | def add_row(self, row_key, init_fields_dict=None): | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 131 | r""" | 
|  | 132 | Add a row to the tally sheet. | 
|  | 133 |  | 
|  | 134 | Description of arguments: | 
| George Keishing | 3996704 | 2023-02-16 00:43:51 -0600 | [diff] [blame] | 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. | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 142 | """ | 
|  | 143 |  | 
| Michael Walsh | 5f4bce8 | 2019-07-16 16:33:01 -0500 | [diff] [blame] | 144 | if row_key in self.__table: | 
|  | 145 | # If we allow this, the row values get re-initialized. | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 146 | message = 'An entry for "' + row_key + '" already exists in' | 
| Michael Walsh | 5f4bce8 | 2019-07-16 16:33:01 -0500 | [diff] [blame] | 147 | message += " tally sheet." | 
|  | 148 | raise ValueError(message) | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 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): | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 157 | r""" | 
|  | 158 | Update a field in a row with the specified value. | 
|  | 159 |  | 
|  | 160 | Description of arguments: | 
| George Keishing | 3996704 | 2023-02-16 00:43:51 -0600 | [diff] [blame] | 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. | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 166 | """ | 
|  | 167 |  | 
|  | 168 | self.__table[row_key][field_key] = value | 
|  | 169 |  | 
|  | 170 | def inc_row_field(self, row_key, field_key): | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 171 | r""" | 
| George Keishing | 3996704 | 2023-02-16 00:43:51 -0600 | [diff] [blame] | 172 | Increment the value of the specified field in the specified row. | 
|  | 173 | The value of the field must be numeric. | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 174 |  | 
|  | 175 | Description of arguments: | 
| George Keishing | 3996704 | 2023-02-16 00:43:51 -0600 | [diff] [blame] | 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. | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 180 | """ | 
|  | 181 |  | 
|  | 182 | self.__table[row_key][field_key] += 1 | 
|  | 183 |  | 
|  | 184 | def dec_row_field(self, row_key, field_key): | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 185 | r""" | 
| George Keishing | 3996704 | 2023-02-16 00:43:51 -0600 | [diff] [blame] | 186 | Decrement the value of the specified field in the specified row. | 
|  | 187 | The value of the field must be | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 188 | numeric. | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 189 |  | 
|  | 190 | Description of arguments: | 
| George Keishing | 3996704 | 2023-02-16 00:43:51 -0600 | [diff] [blame] | 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. | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 195 | """ | 
|  | 196 |  | 
|  | 197 | self.__table[row_key][field_key] -= 1 | 
|  | 198 |  | 
|  | 199 | def calc(self): | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 200 | r""" | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 201 | Calculate totals and row calc fields.  Also, return totals_line dictionary. | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 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: | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 209 | tokens = [i for i in re.split(r"(\d+|\W+)", calc_field) if i] | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 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). | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 217 | cmd_buf += ( | 
|  | 218 | "self._" | 
|  | 219 | + self.__class__.__name__ | 
|  | 220 | + "__table['" | 
|  | 221 | + row_key | 
|  | 222 | + "']['" | 
|  | 223 | + token | 
|  | 224 | + "'] " | 
|  | 225 | ) | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 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): | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 235 | r""" | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 236 | sprint the fields of this object.  This would normally be for debug purposes. | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 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): | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 254 | r""" | 
| Michael Walsh | 410b178 | 2019-10-22 15:56:18 -0500 | [diff] [blame] | 255 | print the fields of this object to stdout.  This would normally be for debug purposes. | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 256 | """ | 
|  | 257 |  | 
|  | 258 | sys.stdout.write(self.sprint_obj()) | 
|  | 259 |  | 
|  | 260 | def sprint_report(self): | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 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()] | 
| Michael Walsh | 86f029f | 2018-03-27 11:58:30 -0500 | [diff] [blame] | 268 | report_width = 40 | 
|  | 269 | key_width = 40 | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 270 | format_string = "{0:<" + str(key_width) + "}" | 
|  | 271 | dash_format_string = "{0:-<" + str(key_width) + "}" | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 272 | field_num = 0 | 
|  | 273 |  | 
| Michael Walsh | 5f4bce8 | 2019-07-16 16:33:01 -0500 | [diff] [blame] | 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): | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 279 | align = ":>" | 
| Michael Walsh | 5f4bce8 | 2019-07-16 16:33:01 -0500 | [diff] [blame] | 280 | else: | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 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 | ) | 
| Michael Walsh | 5f4bce8 | 2019-07-16 16:33:01 -0500 | [diff] [blame] | 288 | report_width += 1 + len(row_key) | 
|  | 289 | col_names.append(row_key.title()) | 
|  | 290 | except StopIteration: | 
|  | 291 | pass | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 292 | num_fields = field_num + 1 | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 293 | totals_line_fmt = "{0:=<" + str(report_width) + "}" | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 294 |  | 
|  | 295 | buffer += format_string.format(*col_names) + "\n" | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 296 | buffer += dash_format_string.format(*([""] * num_fields)) + "\n" | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 297 | for row_key, value in self.__table.items(): | 
|  | 298 | buffer += format_string.format(row_key, *value.values()) + "\n" | 
|  | 299 |  | 
| Patrick Williams | 20f3871 | 2022-12-08 06:18:26 -0600 | [diff] [blame] | 300 | buffer += totals_line_fmt.format("") + "\n" | 
|  | 301 | buffer += ( | 
|  | 302 | format_string.format("Totals", *self.__totals_line.values()) + "\n" | 
|  | 303 | ) | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 304 |  | 
|  | 305 | return buffer | 
|  | 306 |  | 
|  | 307 | def print_report(self): | 
| Michael Walsh | fefcbca | 2016-11-11 14:28:34 -0600 | [diff] [blame] | 308 | r""" | 
|  | 309 | print the tally sheet in a formatted way. | 
|  | 310 | """ | 
|  | 311 |  | 
|  | 312 | sys.stdout.write(self.sprint_report()) |