PK=POP?4vEErtm/__init__.py""" Validate a Requirements Trace Matrix """ __version__ = "0.1.26" PKm9POEaartm/containers/field.py"""This module defines the Field class, a base class that all fields will build on. See __init__ for more detail.""" # --- Standard Library Imports ------------------------------------------------ from collections import defaultdict, namedtuple # --- Third Party Imports ----------------------------------------------------- # None # --- Intra-Package Imports --------------------------------------------------- import rtm.main.context_managers as context from rtm.containers.worksheet_columns import get_matching_worksheet_columns from rtm.validate import validator_output from rtm.main import config from rtm.validate.validator_output import OutputHeader import rtm.validate.validation as val class Field: def __init__(self, name): """All columns (except for the Cascade Block) in the RTM excel worksheet are represented by this class. Objects of this class record the header, the cell values, the columns horizontal position, etc. """ self.name = name # --- Get matching columns -------------------------------------------- # The excel sheet might contain multiple columns that match this # field's name. We save all of them here, but only the first one gets used. # If there's more than one match, the user is alerted. # TODO Make sure this is actually true. matching_worksheet_columns = get_matching_worksheet_columns( context.worksheet_columns.get(), name, ) # --- Override defaults if matches are found -------------------------- if len(matching_worksheet_columns) >= 1: # Get all matching indices (used for checking duplicate data and proper sorting) # Used in later checks of relative column position self._positions = [col.position for col in matching_worksheet_columns] # From first matching column, get cell values for rows 2+ # Get first matching column data # Duplicate columns are ignored; user receives warning. self.values = matching_worksheet_columns[0].values else: self._positions = None self.values = None # --- Set up for validation ------------------------------------------- self._correct_position = None self._val_results = [] @property def found(self): """True if at least one RTM column has self.name as its header""" if self.values is None: return False return True @property def position_left(self): """Return horizontal/columnar position of this field. Used to make sure this column is in the correct position relative to the two fields expected to be to the left and right.""" if self.found: return self._positions[0] else: return -1 @property def position_right(self): """For single-column fields (i.e. most fields), the left and right positions are the same. Multi-column fields (i.e. Cascade Block) return different values for left and right positions.""" return self.position_left @property def column(self): return self.position_left + 1 def print(self): """Print to console 1) field name and 2) the field's validation results.""" for result in self._val_results: result.print() @property def excel_markup(self): """Return dict. Key= row number. Value= List of comments.""" markup = defaultdict(list) for val_result in self._val_results[1:]: # exclude the first item, which is just the header val_result: validator_output.ValidationResult if val_result.score == 'Pass': continue val_type = val_result.excel_type if val_type == 'body': col = self.column comment = (val_result.title, val_result.comment) for row in val_result.rows: location = (row, col) markup[location].append(comment) elif val_type == 'header': row = config.header_row col = self.column location = (row, col) comment = (val_result.title, val_result.comment) markup[location].append(comment) elif val_type == 'notes': location = self.name comment = (val_result.title, val_result.comment) markup[location].append(comment) else: raise ValueError(f"{val_type} is not a valid validation type.") return markup def __str__(self): return self.__class__, self.found def val_results_header_and_field_exists(self): return [OutputHeader(self.name), val.field_exist(self.found, self.name)] if __name__ == "__main__": a = {1, 2} b = 'a b'.split() b += a print(b) PKm9PO|"EErtm/containers/fields.py"""This module contains two types of classes: the Fields sequence class, and the fields it contains. Fields represent the columns (or groups of columns) in the RTM worksheet.""" # --- Standard Library Imports ------------------------------------------------ import collections import functools # --- Third Party Imports ----------------------------------------------------- # None # --- Intra-Package Imports --------------------------------------------------- import rtm.containers.field as ft import rtm.validate.validation as val from rtm.main import context_managers as context class Fields(collections.abc.Sequence): # --- Collect field classes ----------------------------------------------- field_classes = [] @classmethod def collect_field(cls): """Append a field class to this Fields sequence.""" def decorator(field_): cls.field_classes.append(field_) return field_ return decorator # --- Field Object Methods def __init__(self): """The Fields class is a sequence of fields. First, the field classes are collected in the order they're expected to appear in the RTM via the `collect_field` decorator. When initialized, all fields in the sequence get initialized too.""" self.height = context.worksheet_columns.get().height # height is the number of rows of values. self._fields = [field_class() for field_class in self.field_classes] def get_field_object(self, field_class): """Given a field class or name of field class, return the matching field object""" for _field in self: if isinstance(field_class, str): if _field.__class__.__name__ == field_class: return _field else: if isinstance(_field, field_class): return _field raise ValueError(f'{field_class} not found in {self.__class__}') def validate(self): """Validate all field objects in this sequence.""" for field in self: field.validate() def print(self): """Output validation results to console for field objects in sequence""" # TODO give this a better name, one that shows that this is command line output for field_ in self: field_.print() @property def excel_markup(self): comments = collections.defaultdict(list) for field in self: for key, value in field.excel_markup.items(): comments[key] += value return comments # --- Sequence ------------------------------------------------------------ def __getitem__(self, item): return self._fields[item] def __len__(self) -> int: return len(self._fields) @Fields.collect_field() class ID(ft.Field): def __init__(self): """The ID field uniquely identifies each row (work item) and should sort alphabetically in case the excel sheet gets accidentally sorted on some other column.""" super().__init__(name="ID") def validate(self): """Validate this field""" work_items = context.work_items.get() self._val_results = self.val_results_header_and_field_exists() if self.found: self._val_results += [ val.left_right_order(self), # No need for explicit "not empty" check b/c this is caught by pxxx # format and val_match_parent_prefix. val.unique(self.values), val.alphabetical_sort(self.values), val.procedure_step_format(self.values, work_items), val.start_w_root_id(), ] def get_index(self, id_value): # TODO Later, improve with a bisect search of a sorted list try: return self.values.index(id_value) except ValueError: return -1 @Fields.collect_field() class CascadeBlock(ft.Field): def __init__(self): """The CascadeBlock is the most complicated of the RTM fields. it spans multiple columns and must help determine the parent for each work item.""" self.name = 'Cascade Block' self._subfields = [] for subfield_name in self._get_subfield_names(): subfield = ft.Field(subfield_name) if subfield.found: self._subfields.append(subfield) else: self.last_field_not_found = subfield_name break @staticmethod def _get_subfield_names(): """Return list of column headers. The first several are required. The last dozen or so are unlikely to be found on the RTM. This is because the user is allowed as many Design Output Solutions as they need.""" field_names = ["Procedure Step", "Need", "Design Input"] for i in range(1, 20): field_names.append(f"Solution Level {str(i)}") return field_names @property def found(self): """True if at least one RTM column was found matching the headers given by self._get_subfield_names""" if len(self) > 0: return True else: return False @property def values(self): """Return a list of lists of cell values (for rows 2+)""" return [subfield.values for subfield in self] @property def position_left(self): """Return position of the first subfield""" if self.found: return self[0].position_left else: return -1 @property def position_right(self): """Return position of the last subfield""" if self.found: return self[-1].position_left else: return -1 # @functools.lru_cache() # TODO how often does get_row get used? If more than once, add lru cache back in? def get_row(self, index) -> list: return [col[index] for col in self.values] def validate(self): """Validate this field""" self._val_results = self.val_results_header_and_field_exists() if self.found: self._val_results += [ val.left_right_order(self), # LEFT/RIGHT ORDER val.cascade_block_not_empty(), # NOT EMPTY val.single_entry(), # SINGLE ENTRY val.use_all_columns(), # USE ALL COLUMNS val.orphan_work_items(), # ORPHAN WORK ITEMS val.solution_level_terminal(), # SOLUTION LEVEL TERMINAL val.f_entry(), # F val.x_entry(), # X ] # --- Sequence ------------------------------------------------------------ def __len__(self): return len(self._subfields) def __getitem__(self, item): return self._subfields[item] @Fields.collect_field() class CascadeLevel(ft.Field): def __init__(self): """The Cascade Level field goes hand-in-hand with the Cascade Block. It even duplicates some information to a degree. It's important that the values in this field agree with those in the Cascade Block.""" super().__init__(name="Cascade Level") def validate(self): """Validate this field""" self._val_results = self.val_results_header_and_field_exists() if self.found: self._val_results += [ val.left_right_order(self), val.not_empty(self.values), val.cascade_level_valid_input(self), val.cascade_block_match(), ] Tag = collections.namedtuple('Tag', 'name index modifier') class Edge: def __init__(self, index, tag_name, other_id, req_statement_field, id_field): """Instances of this class track a single secondary edge between parent and child documented as tags in the ReqStatement field. It requires the ReqStatement field to have already parsed its tags into a common format of index, tag_name, and modifier (in this case: the ID pointer)""" req_statement_field: ReqStatement id_field: ID self.req_statement_field = req_statement_field self.id_field = id_field self.index = index self.tag_name = tag_name # Parent or Child self.other_id = other_id self.category = tag_name # self.get_other_edge_tag(req_statement_field.edge_tag_names) self.other_index = self.get_other_index(other_id, id_field.values) @property def id_value(self): return self.id_field.values[self.index] @staticmethod def get_other_index(other_id, id_values): for index, id_value in enumerate(id_values): if id_value == other_id: return index return -1 # def get_other_edge_tag(self, both_edge_tag_names): # edge_tags = set(both_edge_tag_names) # create shallow copy # try: # edge_tags.remove(self.tag_name) # except KeyError: # return '' # return edge_tags.pop() @property def connected(self): """Connected means the ID (e.g. P010-0020) passed to this edge exists.""" return True if self.other_index != -1 else False @property def mutual(self): return False if self.target_edge is None else True @property @functools.lru_cache() def target_edge(self): if not self.connected: return None all_edges = self.req_statement_field.edges.as_dict if self.other_index not in all_edges: return None edges_at_target = all_edges[self.other_index] for edge in edges_at_target: if edge.other_index == self.index: return edge return None def __repr__(self): return f"<{self.index}/{self.id_value} " \ f"{'mutual ' if self.mutual else ''}" \ f"{self.category} of {self.other_index}/{self.other_id}>" class Edges: def __init__(self, req_statement_field, id_field): """This container class stores the data methods""" req_statement_field: ReqStatement id_field: ID self._edges = collections.defaultdict(list) # Populate self._edges for tag in req_statement_field.tags: tag: Tag if tag.name not in req_statement_field.edge_tag_names: continue self._edges[tag.index].append(Edge( index=tag.index, tag_name=tag.name, other_id=tag.modifier, req_statement_field=req_statement_field, id_field=id_field, )) @property def as_list(self): list_of_edges = [] for edges in self._edges.values(): list_of_edges += edges return list_of_edges @property def as_dict(self): return self._edges @Fields.collect_field() class ReqStatement(ft.Field): def __init__(self): """The Requirement Statement field is basically a large text block. Besides the req statement itself, it also contains tags, such as for marking additional parents and children.""" super().__init__("Requirement Statement") self.edge_tag_names = {'ParentOf', 'ChildOf'} self.allowed_tag_names = self.edge_tag_names | { 'Function', 'MatingParts', 'MechProperties', 'UserInterface', } self._tags = None self._edges = None def validate(self): """Validate this field""" self._val_results = self.val_results_header_and_field_exists() if self.found: self._val_results += [ val.left_right_order(self), # LEFT/RIGHT ORDER val.not_empty(self.values), # NOT EMPTY val.missing_tags(), # MISSING TAGS val.custom_tags(), # CUSTOM TAGS val.parent_child_modifiers(), # PARENT/CHILD MODIFIERS val.mutual_parent_child(), # MUTUAL PARENT/CHILD ] @staticmethod def convert_to_hashtag(tags): return set( '#' + tag for tag in tags ) @property def tags(self): if self._tags is None: self._tags = [] # Extract tags -------------------------------------------------------- for index, cell_value in enumerate(self.values): # --- split up cell string ---------------------------------------- try: cell_lines = cell_value.split('\n') except AttributeError: continue for line in cell_lines: words = line.split() # --- Check for tag ------------------------------------------- if len(words) > 0: potential_tag = words[0] else: continue if potential_tag.startswith('#'): tag_name = potential_tag[1:] modifier = words[1] if len(words) > 1 else '' # --- Save tag, index, and modifier ----------------------- self._tags.append(Tag(tag_name, index, modifier)) return self._tags @property # @functools.lru_cache() def tags_ven_diagram(self): allowed_tags = self.allowed_tag_names actual_tags = set(tag.name for tag in self.tags) TagNames = collections.namedtuple('TagNames', 'base actual base_found missing additional') return TagNames( base=allowed_tags, actual=actual_tags, base_found=actual_tags & allowed_tags, missing=allowed_tags - actual_tags, additional=actual_tags - allowed_tags, ) @property def edges(self): if self._edges is None: fields: Fields = context.fields.get() self._edges = Edges( req_statement_field=self, id_field=fields.get_field_object('ID') ) return self._edges @Fields.collect_field() class ReqRationale(ft.Field): def __init__(self): """This field has very few requirements.""" super().__init__(name="Requirement Rationale") def validate(self): """Validate this field""" self._val_results = self.val_results_header_and_field_exists() if self.found: self._val_results += [ val.left_right_order(self), val.not_empty(self.values), ] @Fields.collect_field() class VVStrategy(ft.Field): def __init__(self): """The V&V Strategy field is subject to few rules.""" super().__init__(name="Verification or Validation Strategy") def validate(self): """Validate this field""" self._val_results = self.val_results_header_and_field_exists() if self.found: self._val_results += [ val.left_right_order(self), val.not_empty(self.values), val.business_need_na(self.values), ] @Fields.collect_field() class VVResults(ft.Field): def __init__(self): """Verification or Validation Results field""" super().__init__(name="Verification or Validation Results") def validate(self): """Validate this field""" self._val_results = self.val_results_header_and_field_exists() if self.found: self._val_results += [ val.left_right_order(self), val.not_empty(self.values), val.business_need_na(self.values), ] @Fields.collect_field() class Devices(ft.Field): def __init__(self): """Devices field""" super().__init__(name="Devices") def validate(self): """Validate this field""" self._val_results = self.val_results_header_and_field_exists() if self.found: self._val_results += [ val.left_right_order(self), val.not_empty(self.values), ] @Fields.collect_field() class DOFeatures(ft.Field): def __init__(self): """"Design Output Features field""" super().__init__(name="Design Output Feature (with CTQ ID #)") def validate(self): """Validate this field""" self._val_results = self.val_results_header_and_field_exists() if self.found: self._val_results += [ val.left_right_order(self), val.not_empty(self.values), val.ctq_format(self.values), val.missing_ctq(self.values), ] @Fields.collect_field() class CTQ(ft.Field): def __init__(self): """CTQ field""" super().__init__(name="CTQ? Yes, No, N/A") def validate(self): """Validate this field""" self._val_results = self.val_results_header_and_field_exists() if self.found: self._val_results += [ val.left_right_order(self), val.not_empty(self.values), val.ctq_valid_input(self.values), val.ctq_to_yes(self.values), ] def get_expected_field_left(field): """Return the field object that *should* come before the argument field object.""" initialized_fields = context.fields.get() index_prev_field = None for index, field_current in enumerate(initialized_fields): if field is field_current: index_prev_field = index - 1 break if index_prev_field is None: raise ValueError elif index_prev_field == -1: return None else: return initialized_fields[index_prev_field] if __name__ == "__main__": pass PKCOOB_rtm/containers/markup.py"""The class below is used to store comments that can be printed either to console or excel.""" # --- Standard Library Imports ------------------------------------------------ import collections # --- Third Party Imports ----------------------------------------------------- # None # --- Intra-Package Imports --------------------------------------------------- # None CellMarkup = collections.namedtuple("CellMarkup", "comment is_error indent size is_bold") # is_error will drive what color to highlight the cell (e.g. green for neutral and orange for error) # indent: indent the cell contents if true CellMarkup.__new__.__defaults__ = ('', False, False, None, False) PKm9POTT[[rtm/containers/work_items.py"""This module defines both the work item class (equivalent to a worksheet row) and the work items class, a custom sequence contains all work items.""" # --- Standard Library Imports ------------------------------------------------ import collections import functools # --- Third Party Imports ----------------------------------------------------- # None # --- Intra-Package Imports --------------------------------------------------- import rtm.main.context_managers as context from rtm.validate.checks import cell_empty CascadeBlockCell = collections.namedtuple("CascadeBlockCell", "depth value") class WorkItem: def __init__(self, index): """A work item is basically a row in the RTM worksheet. It's an item that likely a parent and at least one child.""" self.index = index # work item's vertical position relative to other work items self.edges = [] @property @functools.lru_cache() def cascade_block_row(self): """Return list of tuples (depth, cell_value)""" cascade_block = context.fields.get().get_field_object('CascadeBlock') cascade_block_row_cells = cascade_block.get_row(self.index) return [ CascadeBlockCell(depth, value) for depth, value in enumerate(cascade_block_row_cells) if not cell_empty(value) ] @property def value(self): try: return self.cascade_block_row[0][1] except IndexError: return None @property def depth(self): """Depth is equivalent to the number of edges from this work item to a root work item.""" try: return self.cascade_block_row[0].depth except IndexError: return None @property def is_root(self): """Is this work item a Procedure Step?""" # TODO covered by test? return True if self.depth == 0 else False @property @functools.lru_cache() def parent(self): # TODO rename to parent """Return parent work item""" # If no position (cascade block row was blank), then no parent if self.depth is None: return MissingWorkItem(self) # If root item (e.g. procedure step), then no parent! if self.is_root: return MissingWorkItem(self) # Search backwards through previous work items work_items = context.work_items.get() for index in reversed(range(self.index)): other = work_items[index] if other.depth is None: # Skip work items that have a blank cascade. Keep looking. continue elif other.depth == self.depth: # same position, same parent return other.parent elif other.depth == self.depth - 1: # one column to the left; that work item IS the parent return other elif other.depth < self.depth - 1: # cur_work_item is too far to the left. There's a gap in the chain. No parent return MissingWorkItem(self) else: # self.position < other.position # Skip work items that come later in the cascade. Keep looking. continue # We should never get to this point. I was going to return a # MissingWorkItem at this point, but I'd rather an exception get thrown # and have to fix it. @property @functools.lru_cache() def root(self): """Return this work item's root item. The 'root' is the parent of the parent of the parent...etc. i.e. the Procedure Step""" if self.is_root: return self return self.parent.root @property def has_root(self) -> bool: # If a work item doesn't have a root, then that error will be caught elsewhere. # No need to return an error here. return self.root is not MissingWorkItem @property def allowed_to_be_terminal_work_item(self) -> bool: return self.depth >= 3 def __repr__(self): return f"" class MissingWorkItem(WorkItem): def __init__(self, creator): super().__init__(-1) # index = -1 (error) self.creator = creator @property def cascade_block_row(self): return [] @property def depth(self): return None @property def is_root(self): return False @property def parent(self): return self @property def root(self): return self class WorkItems(collections.abc.Sequence): def __init__(self): """Sequence of work items""" self._work_items = [WorkItem(index) for index in range(context.fields.get().height)] @property def child_count(self) -> dict: result = collections.defaultdict(int) for work_item in self: result[work_item.parent.index] += 1 return result @property def childless_items(self) -> list: all_indices = set(range(len(self))) indices_with_children = set(self.child_count.keys()) childless_indices = sorted(list(all_indices - indices_with_children)) return [self[index] for index in childless_indices] @property def leaf_items(self): # Leaves meet two criteria: # No children # Has a root item # Isn't a room item (which is actually redundant given the above check) # A "terminal" work item is a leaf in a graph: http://mathworld.wolfram.com/TreeLeaf.html return [work_item for work_item in self.childless_items if work_item.has_root] # --- Sequence ------------------------------------------------------------ def __getitem__(self, item) -> WorkItem: return self._work_items[item] def __len__(self) -> int: return len(self._work_items) if __name__ == "__main__": pass # class TestThingee: # def __init__(self): # self.stuff = 1 # # @property # def stuff(self): # return 2 # # thingee = TestThingee() # print(thingee.stuff) PK;POB #rtm/containers/worksheet_columns.py"""This module defines the worksheet column class and the custom sequence containing all the worksheet columns. Worksheet columns are containers housing a single column from a worksheet.""" # --- Standard Library Imports ------------------------------------------------ from collections import namedtuple from typing import List # --- Third Party Imports ----------------------------------------------------- import openpyxl # --- Intra-Package Imports --------------------------------------------------- import rtm.main.config as config WorksheetColumn = namedtuple("WorksheetColumn", "header values position column") # header: row 1 # values: list of cell values starting at row 2 # position: similar to column number, but starts as zero, like an index # column: column number (start at 1) class WorksheetColumns: def __init__(self, worksheet): # --- Attributes ------------------------------------------------------ self.max_row = worksheet.max_row self.height = self.max_row - config.header_row self.cols = [] # --- Convert Worksheet to WorksheetColumn objects ---------------- start_column_num = 1 for position, col in enumerate(range(start_column_num, worksheet.max_column + 1)): column_header = str(worksheet.cell(config.header_row, col).value) column_values = tuple(worksheet.cell(row, col).value for row in range(config.header_row+1, self.max_row + 1)) ws_column = WorksheetColumn( header=column_header, values=column_values, position=position, column=col ) self.cols.append(ws_column) def get_first(self, header_name): """returns the first worksheet_column that matches the header""" matches = get_matching_worksheet_columns(self, header_name) if len(matches) > 0: return matches[0] else: return None # --- Sequence ------------------------------------------------------------ def __getitem__(self, index): return self.cols[index] def __len__(self): return len(self.cols) def get_matching_worksheet_columns(sequence_worksheet_columns, field_name) -> List[WorksheetColumn]: """Called by constructor to get matching WorksheetColumn objects""" matching_worksheet_columns = [ ws_col for ws_col in sequence_worksheet_columns if ws_col.header.lower() == field_name.lower() ] return matching_worksheet_columns PKCOO!Xrtm/main/api.py"""The main() function here defines the structure of the RTM Validation process. It calls the main steps in order and handles exceptions directly related to validation errors (e.g. missing columns)""" # --- Standard Library Imports ------------------------------------------------ # None # --- Third Party Imports ----------------------------------------------------- import click # --- Intra-Package Imports --------------------------------------------------- import rtm.main.excel as excel from rtm.main.exceptions import RTMValidatorError from rtm.containers.fields import Fields import rtm.containers.worksheet_columns as wc import rtm.containers.work_items as wi import rtm.main.context_managers as context from rtm.main.versions import print_version_check_message def main(highlight_bool=False, highlight_original=False, path=None): """This is the main function called by the command line interface.""" click.clear() click.echo( "\nWelcome to the DePuy Synthes Requirements Trace Matrix (RTM) Validator." "\nPlease select an RTM excel file you wish to validate." ) click.echo() print_version_check_message() if highlight_original: highlight_original = click.confirm('Are you sure you want to edit the original excel file? Images, etc will be lost.') try: if not path: path = excel.get_rtm_path() wb = excel.get_workbook(path) ws = excel.get_worksheet(wb, "Procedure Based Requirements") worksheet_columns = wc.WorksheetColumns(ws) with context.worksheet_columns.set(worksheet_columns): fields = Fields() with context.fields.set(fields): work_items = wi.WorkItems() with context.fields.set(fields), context.work_items.set(work_items): fields.validate() fields.print() if highlight_bool: excel.mark_up_excel(path, wb, ws, fields.excel_markup, highlight_original) except RTMValidatorError as e: click.echo(e) click.echo( "\nThank you for using the RTM Validator." "\nIf you have questions or suggestions, please contact a Roebling team member.\n" ) if __name__ == "__main__": main() PK Path: """Prompt user for RTM workbook location. Return path object.""" if path_option == 'default': path = get_new_path_from_dialog() required_extensions = '.xlsx .xls'.split() if str(path) == '.': raise exc.RTMValidatorFileError("\nError: You didn't select a file") if path.suffix not in required_extensions: raise exc.RTMValidatorFileError( f"\nError: You didn't select a file with " f"a proper extension: {required_extensions}" ) click.echo(f"\nThe RTM you selected is {path}") return path elif isinstance(path_option, Path): return path_option def get_new_path_from_dialog() -> Path: """Provide user with dialog box so they can select the RTM Workbook""" root = tk.Tk() root.withdraw() path = Path(filedialog.askopenfilename()) return path def get_workbook(path): return openpyxl.load_workbook(filename=str(path), data_only=True) def get_worksheet(workbook, worksheet_name): ws = None for sheetname in workbook.sheetnames: if sheetname.lower() == worksheet_name.lower(): ws = workbook[sheetname] if ws is None: raise exc.RTMValidatorFileError( f"\nError: Workbook does not contain a '{worksheet_name}' worksheet" ) return ws def now_str(pretty=False): if pretty: return datetime.datetime.now().strftime("%d %B %Y, %I:%M %p") else: return datetime.datetime.now().strftime("%Y%m%d_%H%M%S") def get_save_path(original_path, modify_original_file=False): """Get the full file path. Create subdirectory if necessary.""" # --- When modifying original file ---------------------------------------- if modify_original_file: return original_path # --- When saving a copy of the original file ----------------------------- original_path = Path(original_path) original_directory = original_path.parent subdirectory = original_directory/'rtm_validator_results' subdirectory.mkdir(exist_ok=True) file_name = f'{now_str()}_{original_path.name}' return subdirectory / file_name def get_cell_comment_string(comments): # comments is a list of title/explanation pairs titles_and_comments = [f"{comment[0].upper()}\n{comment[1]}" for comment in comments] comments_string = '\n\n'.join(titles_and_comments) return f"{now_str(pretty=True)}\n\n{comments_string}" def mark_up_excel(path, wb, ws_procedure, markup_content: dict, modify_original_file=False): # Comments fall in two categories: # PROCEDURE BASED REQUIREMENTS: These are comments bound to record in a specific field. # The cell gets highlighted orange and a comment explains the error. # README: These are not directed at a specific cell. # These will generate new rows inserted at the top of the worksheet. bg_error = Color('00edc953') # yellow-ish background color for procedure errors fg_good = Color('0025c254') # green-ish text color for readme comments fg_error = Color('00bf2f24') # red-ish text color for readme errors # --- Procedure markup ---------------------------------------------------- for location, comments in markup_content.items(): if not isinstance(location, str): # i.e. if this items has a row/col location cell = ws_procedure.cell(*location) # cell.style = style_error cell.fill = PatternFill(patternType='solid', fgColor=bg_error) cell.comment = Comment(get_cell_comment_string(comments), "RTM Validator") # --- Set up README errors ------------------------------------------------ general_errors = [] for field_name, comments in markup_content.items(): if isinstance(field_name, str): general_errors.append(CellMarkup(field_name.upper(), is_error=True)) for comment in comments: comment_str = f"{comment[0].upper()}: {comment[1]}" general_errors.append(CellMarkup(comment_str, is_error=True, indent=True)) # --- Set up README comments ---------------------------------------------- readme_text = [ CellMarkup("RTM VALIDATOR", size=24), CellMarkup(f"{now_str(pretty=True)}"), CellMarkup("All images and attachments have been removed from this workbook."), CellMarkup(), CellMarkup("Cells highlighted orange require attention."), CellMarkup("See the cell's note/comment for details."), CellMarkup(), CellMarkup("To improve readability, convert notes to comments:"), CellMarkup("Go to the Review tab", indent=True), CellMarkup("Click on Notes, select Convert to Comments", indent=True), CellMarkup(), ] + get_version_check_message() if general_errors: readme_text += [ CellMarkup(), CellMarkup("General Errors:"), CellMarkup(), ] + general_errors # --- create and write to README sheet ------------------------------------ readme = 'README' ws_readme = wb.create_sheet(readme, 0) for row, comment in enumerate(readme_text, 1): cell = ws_readme.cell(row, 1, comment.comment) cell.alignment = Alignment( wrapText=False, indent=3 if comment.indent else 0, ) cell.font = Font( color=fg_error if comment.is_error else fg_good, size=comment.size, bold=True, ) if comment.size: ws_readme.row_dimensions[row].height = comment.size * 1.4 # --- Delete Unmarked Sheets ---------------------------------------------- if not modify_original_file: for worksheet in wb.worksheets: if worksheet not in [ws_procedure, ws_readme]: wb.remove(worksheet) sheet_index = 0 wb.active = sheet_index # --- Save ---------------------------------------------------------------- save_path = get_save_path(path, modify_original_file) wb.save(save_path) os.startfile(save_path) # open(save_path) def row_heights(ws): heights = [ws.row_dimensions[index+1].height for index in range(ws.max_row)] return [15 if height is None else height for height in heights] if __name__ == '__main__': pass PK;7Ohs@qrtm/main/exceptions.py"""Custom exceptions mostly for handling validation errors (e.g. missing worksheet or columns).""" # --- Standard Library Imports ------------------------------------------------ # None # --- Third Party Imports ----------------------------------------------------- # None # --- Intra-Package Imports --------------------------------------------------- # None class RTMValidatorError(Exception): pass class RTMValidatorFileError(RTMValidatorError): """ Raise this for any errors related to the excel file itself. Examples: wrong file extension file missing missing worksheet """ pass class UninitializedError(Exception): pass PK7DOOeFrtm/main/versions.py"""Custom exceptions mostly for handling validation errors (e.g. missing worksheet or columns).""" # --- Standard Library Imports ------------------------------------------------ import collections import functools # --- Third Party Imports ----------------------------------------------------- import click import requests # --- Intra-Package Imports --------------------------------------------------- from rtm import __version__ as installed_version from rtm.containers.markup import CellMarkup @functools.lru_cache() def get_versions(): """Return tuple of installed and pypi version numbers.""" Versions = collections.namedtuple("Versions", "installed pypi") response = requests.get("https://pypi.org/pypi/dps-rtm/json") return Versions( installed=installed_version, pypi=response.json()['info']['version'], ) def get_version_check_message(): versions = get_versions() if versions.pypi == versions.installed: return [ CellMarkup(f"Your app is up to date ({versions.installed})"), ] else: return [ CellMarkup("Your app is out of date.", is_error=True), CellMarkup(f"Currently installed: {versions.installed}", indent=True), CellMarkup(f"Available: {versions.pypi}", indent=True), CellMarkup("Upgrade to the latest by entering the following:", indent=True), CellMarkup("pip install --upgrade dps-rtm", indent=True), ] def print_version_check_message(): """Command Line Output: is app up to date?""" for line in get_version_check_message(): if line.is_error: click.secho(line.comment, fg='red', bold=True,) else: click.echo(line.comment) PKm9POV41ttrtm/validate/checks.py"""Whereas the validation functions return results that will be outputted by the RTM Validator, these "checks" functions perform smaller tasks, like checking individual cells.""" # --- Standard Library Imports ------------------------------------------------ import collections # --- Third Party Imports ----------------------------------------------------- # None # --- Intra-Package Imports --------------------------------------------------- def cell_empty(value) -> bool: """Checks if a cell is empty. Cells contain True or False return False""" if isinstance(value, bool): return False if not value: return True return False def id_prefix_format(self_id, root_id) -> bool: """This is used to check if a work item's ID starts with the root item's ID. This function assumes that the root ID must always be a string.""" # if not isinstance(root_id, str): # return False try: prefix_len = len(root_id) self_prefix = self_id[:prefix_len] except TypeError: return False result = True if self_prefix == root_id else False return result def values_in_acceptable_entries(sequence, allowed_values) -> bool: """Each value in the sequence must be an allowed values. Otherwise, False.""" if len(sequence) == 0: return True for item in sequence: if item not in allowed_values: return False return True # I would like to have included this variable in the CascadeLevel, but that # would cause circular references. allowed_cascade_levels = { # keys: level, values: position 'PROCEDURE STEP': [0], 'USER NEED': [1], 'BUSINESS NEED': [1], 'RISK NEED': [1], 'REGULATORY NEED': [1], 'DESIGN INPUT': [2], 'DESIGN SOLUTION': list(range(3, 20)) } def numbers(): print("Called") return list(range(10)) if __name__ == "__main__": pass PK;POSSrtm/validate/validation.py"""Each of these functions checks a specific aspect of an RTM field and returns a ValidationResult object, ready to be printed on the terminal as the final output of this app.""" # --- Standard Library Imports ------------------------------------------------ import collections import re import functools # --- Third Party Imports ----------------------------------------------------- # None # --- Intra-Package Imports --------------------------------------------------- import rtm.containers.fields import rtm.validate.checks as checks import rtm.main.context_managers as context from rtm.validate.validator_output import ValidationResult from rtm.containers.work_items import MissingWorkItem from rtm.main import config # --- General-purpose validation ---------------------------------------------- def field_exist(field_found, field_name=None) -> ValidationResult: """Given field_found=True/False, return a ValidationResult, ready to be printed to console.""" if field_found: score = "Pass" explanation = None else: score = "Error" explanation = f"Field not found. Either your header does not exactly match '{field_name}' or it is not located in row {config.header_row}." return ValidationResult( score=score, title="Field Exist", cli_explanation=explanation, markup_type='notes', markup_explanation=explanation, ) def left_right_order(field_self) -> ValidationResult: """Does this field actually appear after the one it's supposed to?""" title = "Left/Right Order" # --- Field that is supposed to be positioned to this field's left field_left = rtm.containers.fields.get_expected_field_left(field_self) if field_left is None: # argument field is supposed to be all the way to the left. # It's always in the correct position. score = "Pass" explanation = "This field appears to the left of all the others" elif field_left.position_right <= field_self.position_left: # argument field is to the right of its expected left-hand neighbor score = "Pass" explanation = ( f"This field comes after the {field_left.name} field as it should" ) else: score = "Error" explanation = f"This field should come after {field_left.name}" return ValidationResult(score, title, explanation, markup_type='header') def not_empty(values) -> ValidationResult: """All cells must be non-empty""" error_indices = [] for index, value in enumerate(values): if checks.cell_empty(value): error_indices.append(index) if not error_indices: score = "Pass" explanation = "All cells are non-blank" else: score = "Error" explanation = "Action Required. The following rows are blank: " return ValidationResult( score, title="Not Empty", cli_explanation=explanation, nonconforming_indices=error_indices, markup_type='body', markup_explanation='This cell must contain a value.' ) # --- ID ---------------------------------------------------------------------- def procedure_step_format(id_values, work_items): """Check all root work items for the correct ID format. This check is made indirectly on all other work items when they're compared back to their own root work item.""" def correctly_formatted(value) -> bool: if not isinstance(value, str) or len(value) != 4: return False x = re.match("P\d\d\d", value) return bool(x) error_indices = [ index for index, value in enumerate(id_values) if work_items[index].is_root and not correctly_formatted(value) ] # Output title = 'Procedure Step Format' if not error_indices: score = "Pass" explanation = "All Procedure Step IDs correctly follow the 'PXYZ' format" else: score = "Error" explanation = "The following Procedure Step IDs do not follow the 'PXYZ' format: " return ValidationResult(score, title, explanation, error_indices, markup_type='body') def unique(values): """Each cell in the ID field should be unique. This ensures 1) they can be uniquely identified and 2) an alphabetical sort on that column is deterministic (has the same result each time).""" title = 'Unique Rows' # Record how many times each value appears tally = collections.defaultdict(list) for index, value in enumerate(values): tally[value].append(index) # For repeated ID's, get all indices except for the first. error_indices = [] for indices in tally.values(): error_indices += indices[1:] error_indices.sort() if not error_indices: score = "Pass" explanation = "All IDs are unique." else: score = "Error" explanation = "The following rows contain duplicate IDs: " return ValidationResult(score, title, explanation, error_indices, markup_type='body') def start_w_root_id(): """Each work item starts with its root ID""" id_values = context.fields.get().get_field_object('ID').values work_items = context.work_items.get() error_indices = [] for index, work_item in enumerate(work_items): # Skip this index if it doesn't have a root. # That error will be caught elsewhere. if isinstance(work_item.root, MissingWorkItem): continue # if work_item.root.index == -1: # continue self_id = id_values[index] root_id = id_values[work_item.root.index] if not checks.id_prefix_format(self_id, root_id): error_indices.append(index) # Output title = "Start with root ID" if not error_indices: score = "Pass" explanation = "Each parent/child pair uses the same prefix (e.g. 'P010-')" else: score = "Error" explanation = "Each parent/child pair must use the same prefix (e.g. 'P010-'). The following rows don't: " return ValidationResult(score, title, explanation, error_indices, markup_type='body') # @functools.lru_cache() def alphabetical_sort(values): """Each cell in the ID field should come alphabetically after the prior one.""" error_indices = [] for index, value in enumerate(values): if index == 0: continue string_pair = [values[index - 1], values[index]] if string_pair != sorted(string_pair): # alphabetical sort check error_indices.append(index) # Output title = 'Alphabetical Sort' if not error_indices: score = "Pass" explanation = "All cells appear in alphabetical order" else: score = "Error" explanation = "The following rows are not in alphabetical order: " return ValidationResult(score, title, explanation, error_indices, markup_type='body') # --- Cascade Block ----------------------------------------------------------- def cascade_block_not_empty() -> ValidationResult: """Each row in cascade block must have at least one entry.""" title = 'Not Empty' error_indices = [ work_item.index for work_item in context.work_items.get() if work_item.depth is None ] if not error_indices: score = "Pass" explanation = "All rows are non-blank" else: score = "Error" explanation = ( "Action Required. The following rows have blank cascade blocks: " ) return ValidationResult(score, title, explanation, error_indices, markup_type='body') def single_entry() -> ValidationResult: """Each row in cascade block must contain only one entry""" error_indices = [ work_item.index for work_item in context.work_items.get() if len(work_item.cascade_block_row) != 1 ] if not error_indices: score = "Pass" explanation = "All rows have a single entry" else: score = "Warning" explanation = ( "Action Required. The following rows are blank or have multiple entries: " ) return ValidationResult( score, title='Single Entry', cli_explanation=explanation, nonconforming_indices=error_indices, markup_type='body', markup_explanation='This Cascade Block contains more than one entry.' ) def use_all_columns() -> ValidationResult: """The cascade block shouldn't have any unused columns.""" # Setup fields fields = context.fields.get() cascade_block = fields.get_field_object('CascadeBlock') subfield_count = len(cascade_block) positions_expected = set(range(subfield_count)) # Setup Work Items work_items = context.work_items.get() positions_actual = set( work_item.depth for work_item in work_items ) missing_positions = positions_expected - positions_actual # Output title = "Use All Columns" if len(missing_positions) == 0: score = "Pass" explanation = f"All cascade levels were used." else: score = "Warning" explanation = f"Some cascade levels are unused" return ValidationResult(score, title, explanation, markup_type='header') def orphan_work_items() -> ValidationResult: title = "Orphan Work Items" score = "Pass" explanation = "Each work item must trace back to a procedure step. " \ "This check is not yet implemented. This is a placeholder." return ValidationResult(score, title, explanation, markup_type='body') @functools.lru_cache() def solution_level_terminal() -> ValidationResult: """Each cascade path must terminate in a Solution Level.""" # Setup work_items = context.work_items.get() terminal_work_items = work_items.leaf_items error_indices = [ work_item.index for work_item in terminal_work_items if not work_item.allowed_to_be_terminal_work_item ] # Output if not error_indices: score = "Pass" explanation = f"All Terminal Work Items are of Cascade Level `Solution Level`" else: score = "Error" explanation = f"The following rows terminate a cascade path prior to Solution Level: " return ValidationResult( score=score, title="Terminal Items", cli_explanation=explanation, nonconforming_indices=error_indices, markup_type='body', markup_explanation='This item should have at least one child.' ) def f_entry() -> ValidationResult: """Terminal work items (i.e. leaf nodes) are marked with 'F' in the Cascade Block.""" # Setup work_items = context.work_items.get() exclude_indices = solution_level_terminal().indices error_indices = [ work_item.index for work_item in work_items.leaf_items if work_item.value != 'F' and work_item.index not in exclude_indices ] # Output title = "Leaf Items = F" if not error_indices: score = "Pass" explanation = f"All Terminal Work Items are marked with an `F`" else: score = "Error" explanation = f"The following rows have a value other than `F`: " return ValidationResult(score, title, explanation, error_indices, markup_type='body') def x_entry() -> ValidationResult: """All non-terminal work items (i.e. non-leaf graph nodes) are marked with 'X'""" # allowed_entries = "X F".split() # # error_indices = [ # index # for index, work_item in enumerate(context.work_items.get()) # if not checks.values_in_acceptable_entries( # sequence=[item.value for item in work_item.cascade_block_row], # allowed_values=allowed_entries, # ) # ] # # # Output # title = "X Entry" # if not error_indices: # score = "Pass" # explanation = f"All entries are one of {allowed_entries}" # else: # score = "Error" # explanation = f"Action Required. The following rows contain something other than the allowed {allowed_entries}: " # return ValidationResult(score, title, explanation, error_indices) title = "X Entry" score = "Pass" explanation = "All other work items are marked with an `X`. " \ "This check is not yet implemented. This is a placeholder." return ValidationResult(score, title, explanation, markup_type='body') # --- Cascade Level ----------------------------------------------------------- def cascade_level_valid_input(field) -> ValidationResult: """Check cascade levels against list of acceptable entries.""" values = field.values allowed_values = checks.allowed_cascade_levels.keys() error_indices = [ index for index, value in enumerate(values) if not checks.cell_empty(value) and value not in allowed_values ] # Output if not error_indices: score = "Pass" explanation = "All cell values are valid" else: score = "Error" explanation = f'The following cells contain values other than the allowed' \ f'\n\t\t\t{list(allowed_values)}:\n\t\t\t' return ValidationResult( score, title='Valid Input', cli_explanation=explanation, nonconforming_indices=error_indices, markup_type='body', markup_explanation=f'This cell contains an incorrect value. Choose from the following: {list(allowed_values)}', ) def cascade_block_match() -> ValidationResult: """A work item's cascade level entry must match its cascade block entry.""" fields = context.fields.get() cascade_level = fields.get_field_object('CascadeLevel') body = cascade_level.values # --- Don't report on rows that failed for other reasons (i.e. blank or invalid input exclude_results = [ not_empty(body), cascade_level_valid_input(cascade_level), cascade_block_not_empty() ] exclude_indices = [] for result in exclude_results: exclude_indices += list(result.indices) indices_to_check = set(range(len(body))) - set(exclude_indices) error_indices = [] work_items = context.work_items.get() for index in indices_to_check: cascade_block_position = work_items[index].depth cascade_level_value = body[index] allowed_positions = checks.allowed_cascade_levels[cascade_level_value] if cascade_block_position not in allowed_positions: error_indices.append(index) # Output title = 'Cascade Block Match' if not error_indices: score = "Pass" explanation = "All rows (that passed previous checks) match the position marked in the Cascade Block" else: score = "Error" explanation = f'The following rows do not match the cascade position marked in the Cascade Block:' return ValidationResult(score, title, explanation, error_indices, markup_type='body') # --- Requirement Statement --------------------------------------------------- def missing_tags() -> ValidationResult: """Report on usage of out-of-the-box tags.""" # --- Get data ------------------------------------------------------------ req_statement = context.fields.get().get_field_object('ReqStatement') tags = req_statement.tags_ven_diagram # Output title = 'Base Tags' if not tags.missing: # All base tags were used score = 'Pass' explanation = f'All base tags found ({list(tags.base)}).' elif not tags.base_found: # No base tags were used score = 'Warning' explanation = f'No base tags were found ({list(tags.base)}).' else: # Some base tags were used score = 'Warning' explanation = f'These base tags were found: {list(tags.base_found)}. ' \ f'These were missing: {list(tags.missing)}' val_result = ValidationResult(score, title, explanation, markup_type='header') val_result.base_found = sorted(list(tags.base_found)) val_result.missing = sorted(list(tags.missing)) return val_result def custom_tags() -> ValidationResult: """Report on usage of custom tags.""" # --- Get data ------------------------------------------------------------ req_statement = context.fields.get().get_field_object('ReqStatement') tags = req_statement.tags_ven_diagram # --- Generate Output ----------------------------------------------------- title = 'Custom Tags' if not tags.additional: # No custom tags score = 'Pass' explanation = 'No custom tags found.' else: # These custom tags were found. score = 'Warning' explanation = f"These custom tags were found: {list(tags.additional)}. " \ f"Make sure you didn't mean to use one of the base tags: {list(tags.base)}" val_result = ValidationResult(score, title, explanation, markup_type='header') val_result.custom = sorted(list(tags.additional)) return val_result def parent_child_modifiers() -> ValidationResult: """The parent and child tags from the Requirement Statement should each point to a valid ID.""" # --- Get data ------------------------------------------------------------ req_statement = context.fields.get().get_field_object('ReqStatement') # --- Look for unconnected edges ------------------------------------------ error_indices = sorted(list(set( edge.index for edge in req_statement.edges.as_list if not edge.connected ))) # Output title = 'Parent/Child Modifiers' if not error_indices: score = 'Pass' explanation = 'All #AdditionalParent and #Child tags match to existing IDs.' else: score = "Error" explanation = "The following rows have #AdditionalParent and #Child tags that don't match to any existing IDs: " return ValidationResult(score, title, explanation, error_indices, markup_type='body') def mutual_parent_child() -> ValidationResult: """The parent and child tags from the Requirement Statement should each point to a valid ID. That target row should have a matching tag that points back.""" # --- Get data ------------------------------------------------------------ req_statement = context.fields.get().get_field_object('ReqStatement') edges = req_statement.edges # --- Exclude indices that already failed other tests --------------------- excluded_indices = [index for index in parent_child_modifiers().indices] # --- Look for non-mutual edges ------------------------------------------- error_indices = sorted(list(set( edge.index for edge in req_statement.edges.as_list if edge.connected and not edge.mutual ))) # Output title = 'Mutual Parent/Child' tags_string = ' and '.join(req_statement.convert_to_hashtag(req_statement.edge_tag_names)) if not error_indices: score = 'Pass' explanation = f'All {tags_string} tags match to existing IDs.' else: score = "Error" explanation = f"The following rows have {tags_string} tags that don't have a matching tag pointing back: " return ValidationResult(score, title, explanation, error_indices, markup_type='body') # --- VorV Strategy, Results -------------------------------------------------- def business_need_na(values) -> ValidationResult: # ... # Output title = 'Business Need N/A' score = 'Pass' explanation = "Business Need work items are marked with 'N/A'. " \ "This check is not yet implemented. This is a placeholder." return ValidationResult(score, title, explanation, markup_type='body') # --- DO Features ------------------------------------------------------------- def ctq_format(values) -> ValidationResult: # ... # Output title = 'CTQ Format' score = 'Pass' explanation = "If contains features that are CTQs, CTQ ID should be formatted as `CTQ##`. " \ "This check is not yet implemented. This is a placeholder." return ValidationResult(score, title, explanation, markup_type='body') def missing_ctq(values) -> ValidationResult: # ... # Output title = 'Missing CTQ' score = 'Pass' explanation = "If CTQ Y/N is `yes`, this column must contain at least one CTQ. " \ "This check is not yet implemented. This is a placeholder." return ValidationResult(score, title, explanation, markup_type='body') # --- CTQ Y/N ----------------------------------------------------------------- def ctq_valid_input(values) -> ValidationResult: # ... # Output title = 'Valid Input' score = 'Pass' explanation = "The only valid inputs for this column are ['yes', 'no', 'N/A', '-']. " \ "This check is not yet implemented. This is a placeholder." # Note: only the procedure step can have a `-` return ValidationResult(score, title, explanation, markup_type='body') def ctq_to_yes(values) -> ValidationResult: # ... # Output title = 'CTQ -> Yes' score = 'Pass' explanation = "Must be 'yes'if the DO Features column contains a CTQ. " \ "This check is not yet implemented. This is a placeholder." return ValidationResult(score, title, explanation, markup_type='body') if __name__ == "__main__": pass PKm9PO$ rtm/validate/validator_output.py"""Instances of these classes contain a single row of validation information, ready to be printed to the terminal at the conclusion of the app.""" # --- Standard Library Imports ------------------------------------------------ import abc from collections import namedtuple from itertools import groupby, count # --- Third Party Imports ----------------------------------------------------- import click # --- Intra-Package Imports --------------------------------------------------- import rtm.main.config as config class ValidatorOutput(metaclass=abc.ABCMeta): @abc.abstractmethod def print(self): return def pretty_int_list(numbers) -> str: def as_range(iterable): """Convert list of integers to an easy-to-read string. Used to display the on the console the rows that failed validation.""" list_int = list(iterable) if len(list_int) > 1: return f'{list_int[0]}-{list_int[-1]}' else: return f'{list_int[0]}' return ', '.join(as_range(g) for _, g in groupby(numbers, key=lambda n, c=count(): n-next(c))) CellResult = namedtuple("CellResult", "row title comment") class ValidationResult(ValidatorOutput): """Each validation function returns an instance of this class. Calling its print() function prints a standardized output to the console.""" def __init__(self, score, title, cli_explanation=None, nonconforming_indices=None, markup_explanation=None, markup_type='body', # other options: 'header', 'notes' ): self._scores_and_colors = {'Pass': 'green', 'Warning': 'yellow', 'Error': 'red'} self.score = score self.title = title self._explanation = cli_explanation self.indices = nonconforming_indices self._comment = markup_explanation self.excel_type = markup_type @property def comment(self): if self._comment is None: return self._explanation return self._comment @property def indices(self): return self.__indices @indices.setter def indices(self, value): if value is not None: self.__indices = list(value) else: self.__indices = [] @property def score(self): return self.__score @score.setter def score(self, value): if value not in self._scores_and_colors: raise ValueError(f'{value} is an invalid score') self.__score = value def _get_color(self): return self._scores_and_colors[self.score] @property def rows(self): first_body_row = 1 + config.header_row # this is the row # directly after the headers return [index + first_body_row for index in self.indices] def print(self) -> None: # --- Print Score in Color ------------------------------------------------ click.secho(f"\t{self.score}", fg=self._get_color(), bold=True, nl=False) # --- Print Rule Title ---------------------------------------------------- click.secho(f"\t{self.title.upper()}", bold=True, nl=False) # --- Print Explanation (and Rows) ---------------------------------------- if self._explanation: click.secho(f' - {self._explanation}{pretty_int_list(self.rows)}', nl=False) click.echo() # new line @property def title_and_comment(self): return f'{self.title.upper()}\n{self.comment}' # def highlight_output(self): # cell_results = [] # for row in self.rows: # cell_result = CellResult(row=row, title=self.title, comment=self._explanation) # cell_results.append(cell_result) # return cell_results class OutputHeader(ValidatorOutput): """Given a field name, print a standardized header on the console.""" def __init__(self, header_name): self.field_name = header_name def print(self) -> None: sym = '+' box_middle = f"{sym} {self.field_name} {sym}" box_horizontal = sym * len(box_middle) click.echo() click.secho(box_horizontal, bold=True) click.secho(box_middle, bold=True) click.secho(box_horizontal, bold=True) click.echo() if __name__ == '__main__': pass PK!H)YD'))dps_rtm-0.1.26.dist-info/entry_points.txtN+I/N.,()**ɵbm&rae z2-t \mWbZ(O=EeNC6\GXxA ٨DcU,U'?zGl X=^ 'E@`!xy`#) B S2ƶ\T> Gan7j R0 g*B@knUX lQdI ٠:.K^OV pbKAbm<ɷ0D=B+пdZ-Ze c2dN;@?xk4Yó]#J4nS{0y׾9'd"4-(%| 6|)D5+O%NM{l6Mx:l;n{gVdTV\g1L_~pLgt>t<8Z `=3_;P9R .B<6frT{$Y,'MbaD=Q~wݬ7e^ qT'n"L!p䍄xm2%gk8q fHeO14fr0|:HG)T& 0T 8@gAFkWc\“yι.dzHʁ3*pYѩ<QwL28" Fxᬊ'a +ͬ\9# z=XV3Dk,Ÿ/ucyƷ{ dHim]3ilN38LNg)M=@,GrAƀor#StWT=>Zȝi:n Ɲ ٧DA:5E"c/ 6UWR~5#No8fWv ڐCR/LB|aoɯ)4.t34(PhRQFSORLJ ?e ̘N;`Zڠ>hVϸv edK yP0QS.eWtEvzZJjbq/OS7_r3@)q-*HOWE 1KW}5rBG_& ZfO_,/|Lh+BjX}f'" ڏs2 N˒ ? 4!t=!w'mVR"+ f<y؟Q`[4Ae% DԩpH q{7Ă8tg$ĩR W=_)e]eqM!OJt'y%`[ n~*lJE0;*_q>@VF(< a=/M]tl^{d3 rLٛ|C3lT"ܩSa{]LѾEwn(<5^3{2Lמ7# I0UTE:9I-``O/Gl'5L 9W"(=gU~'o@w* e`D~oiݞ c;&tcj}Ə~ZA0Qt+Ѿ掮|*S҈ PeA9~OhcXuB$׊ ' -;}Gg׸)zk}]TdI8BeY!Ѥ{A$ n1LjS$rd&_n!e䆟-R#;J-1 α`աVˡy&R-8fꮀbTwJur-vi=!NgJޱi$d&bCחtl?72 8nBM 6 vt L{`Iݝ0 u|sh9: c(X9&)(FI TRTLҋ"bOSjTtxEyfzL[!| c,S 6JL*gj8t~(Ə^tt{F-֪%܁ߘ[U%ސܵ/sÇfѥ9%(Q oa^%zܕ/P:TYs* Z{ne\ŹSciWMJE+@m`$#{̂G,h``.Nv>˳yUcam7n% K,w >_(;0$}s"{+8Ͻ%n-5yV$U'_oEELG}P\.,QW6rIo$DݷA<+.e,)Ww8,/OPK!Hgadps_rtm-0.1.26.dist-info/RECORD}ɖH}> f,z! N0#2$<}{tvQՕ1 POl,!A3^GҳN.gtu|}O*ތ\1^JҵVm6yҕ&j..mz8N8Dx3޸zQr5pluΥ`zǢ1&1i`9]lPOJ<aγf=3zŎW,FQw@ahHJ'A2FNpdH7 cH3*~9ŤHA|jKQD ac& sa W߬-25u{c%[2'9nE_o~`3U)DGRfINt$kO2U>a>7; @