PK O0@JJrtm/__init__.py""" Validate a Requirements Trace Matrix """ __version__ = "0.1.14" PK Oī@N@N rtm/_old/check_form_functions.py""" This script serves as a library of functions for FDR checker. Each function has two comments before it, the first is which column it is intended for and the second states what it does. unless otherwise stated, each function returns a boolean true or false """ # functions start here # Any # take value_at_index in and return it with no carriage returns (\n). This will make strings easier to evaluate def remove_carriage_returns(value): value = value.replace("\n", "") return value # Any # check if empty. returns True if empty def is_empty(value): if not value: return True else: return False # Any # check if value_at_index is N/A. # FDR rules: type of requirement/other circumstances may/may not allow N/A in certain fs def is_notapplic(value): # remove whitespace for direct string comparison. e.g. 'n / a ' becomes 'n/a' value = value.replace(" ", "") # compare lower case version of cell contents to 'n/a'. if value.lower() == "n/a": return True else: return False # Any # check if value_at_index is explicitly a hyphen # FDR rules: if row is a procedure step, all columns besides ID, cascade visualizer, cascade level and requirement # statement should be a hyphen def is_hypen(value): # remove whitespace for direct string comparison. e.g. ' - ' becomes '-' value = value.replace(" ", "") if value == "-": return True else: return False # Any # check if value_at_index is yes def is_yes(value): # remove whitespace for direct string comparison. e.g. 'yes ' becomes 'yes' value = value.replace(" ", "") if value.lower() == "yes": return True else: return False # Any # check if value_at_index is no def is_no(value): # remove whitespace for direct string comparison. e.g. 'no ' becomes 'no' value = value.replace(" ", "") if value.lower() == "no": return True else: return False # Any # check if value_at_index contains 'not required' in its text # FDR rules: some fs are not required. e.g. validation is not required if requirement is a business need def has_not_required(value): if value.lower().find("not required") != -1: return True else: return False # ID # check that value_at_index has a capital P as the first character. # FDR rules: recommended ID formatting for procedure steps and procedure based requirements follow a naming convention. # e.g. P010, P020, etc. for procedure steps and P010-020 for procedure based requirements def starts_with_p(value): if value.startswith("P"): return True else: return False # ID # check if value_at_index has integers following the first letter # FDR rules: recommended ID formatting for procedure steps follow a naming convention. # e.g. P010, P020, etc. for procedure steps def has_digits_after_first(value): return value[1:].isdigit() # ID # check if value_at_index has 3 integers following the first character. First char is omitted # FDR rules: recommended ID formatting for procedure steps follow a naming convention. # e.g. P010, P020, etc. for procedure steps def has_three_digits(value): str1 = value[1:] if (len(str1) == 3) and (str1.isdigit() is True): return True else: return False # ID # check if value_at_index has 6 integers following the first character. # FDR rules: recommended ID formatting for procedure based requirements follow a naming convention. # e.g. P010-020, P010-030, etc. for procedure based requirements # NOTE: First char is omitted. Assumes there is a dash and removes it def has_six_digits(value): # slice string. keep all characters after the first. (removes P) value_slice = value[1:] # find the location/get_index of the hypen within the string. dash_index = value_slice.find("-") # slice string around the hyphen. this will leave only the numeric characters if ID if formatted correctly value_slice = value_slice[:dash_index] + value_slice[dash_index + 1:] if (len(value_slice) == 6) and (value_slice.isdigit() is True): return True else: return False # ID # check for hyphen within string # FDR rules: recommended ID formatting for procedure based requirements follow a naming convention. # e.g. P010-020, P010-030, etc. for procedure based requirements def has_hyphen(value): if value.find("-") != -1: return True else: return False # ID # Check for dash in 4th position (P010-001) # FDR rules: recommended ID formatting for procedure based requirements follow a naming convention. # e.g. P010-020, P010-030, etc. for procedure based requirements def has_hyphen_positioned(value): if value.find("-") == 4: return True else: return False # Cascade Block # check for capital X # FDR rules: only a capital X or capital F are allowed in the cascade visualizer columns. (B-G in its current form) # TODO Question: "has" implies there are allowed to be other chars in the string as well. # TODO I forget how, but there's a better way of removing that whitespace. Strip, maybe? def has_capital_x(value): # remove whitespace for direct string comparison. e.g. ' X ' becomes 'X' value = value.replace(" ", "") if value == "X": return True else: return False # Cascade Block # check for lowercase x # FDR rules: only a capital X or capital F are allowed in the cascade visualizer columns. (B-G in its current form) def has_lower_x(value): # remove whitespace for direct string comparison. e.g. ' x ' becomes 'x' value = value.replace(" ", "") if value == "x": return True else: return False # Cascade Block # check for capital F # FDR rules: only a capital X or capital F are allowed in the cascade visualizer columns. (B-G in its current form) def has_capital_f(value): # remove whitespace for direct string comparison. e.g. ' F ' becomes 'F' value = value.replace(" ", "") if value == "F": return True else: return False # Cascade Block # check for lowercase f # FDR rules: only a capital X or capital F are allowed in the cascade visualizer columns. (B-G in its current form) def has_lower_f(value): # remove whitespace for direct string comparison. e.g. ' f ' becomes 'f' value = value.replace(" ", "") if value == "f": return True else: return False # Cascade level # check if cascade level is 'procedure step' # FDR rules: cascade level defines the type of requirement and can only contain one of the following strings: # procedure step, user need, risk need, business need, design input or design output def is_procedure_step(value): # remove whitespace at the beginning and end of the string and test for value_at_index if value.strip().lower() == "procedure step": return True else: return False # Cascade Level # check if cascade level is 'user need' # FDR rules: cascade level defines the type of requirement and can only contain one of the following strings: # procedure step, user need, risk need, business need, design input or design output def is_user_need(value): # remove whitespace at the beginning and end of the string and test for value_at_index if value.strip().lower() == "user need": return True else: return False # Cascade Level # check if cascade level is 'risk need' # FDR rules: cascade level defines the type of requirement and can only contain one of the following strings: # procedure step, user need, risk need, business need, design input or design output def is_risk_need(value): # remove whitespace at the beginning and end of the string and test for value_at_index if value.strip().lower() == "risk need": return True else: return False # Cascade Level # check if cascade level is 'business need' # FDR rules: cascade level defines the type of requirement and can only contain one of the following strings: # procedure step, user need, risk need, business need, design input or design output def is_business_need(value): # remove whitespace at the beginning and end of the string and test for value_at_index if value.strip().lower() == "business need": return True else: return False # Cascade Level # check if cascade level is 'design input' # FDR rules: cascade level defines the type of requirement and can only contain one of the following strings: # procedure step, user need, risk need, business need, design input or design output def is_design_input(value): # remove whitespace at the beginning and end of the string and test for value_at_index if value.strip().lower() == "design input": return True else: return False # Cascade Level # check if cascade level is 'design output solution' # FDR rules: cascade level defines the type of requirement and can only contain one of the following strings: # procedure step, user need, risk need, business need, design input or design output def is_design_output(value): # remove whitespace at the beginning and end of the string and test for value_at_index if value.strip().lower() == "design output solution": return True else: return False # Cascade level # check if cascade level is one of the approved options. # returns true if it is procedure step, user need, risk need, business need, design input or design output # FDR rules: cascade level may only be one of the 6 defined types. def is_cascade_lvl_approved(value): if is_procedure_step(value) \ ^ is_user_need(value) \ ^ is_risk_need(value) \ ^ is_business_need(value) \ ^ is_design_input(value) \ ^ is_design_output(value) is True: return True else: return False # V&V Results # check if W/C,wc or windchill is present. should indicate if windchill number is present # FDR rules: Design inputs and outputs may reference a document in windchill for its verification/validation results def has_w_slash_c(value): # convert input argument to all lower case for comparison val_lower = value.lower() if val_lower.find("w/c") != -1: return True elif val_lower.find("wc") != -1: return True elif val_lower.find("windchill") != -1: return True else: return False # V&V # check if 10 digit windchill number is present. example W/C# 0000006634 def is_windchill_number_present(value): # remove all spaces value = value.replace(" ", "") # find get_index of 000. windchill numbers have at least three leading zeros. leading_zeros_index = value.find("000") # slice the string starting at that get_index until the end of the string value = value[leading_zeros_index:] # slice string again into two parts. first 10 characters (possible WC number) and remaining characters wc_number = value[:9] remaining_char = value[10:] # test if wc_number is all set_and_get_funcs and remaining is all letters if wc_number.isdigit() and (remaining_char.isalpha() or len(remaining_char) == 0) is True: return True else: return False # Design Output Feature # check for CTQ IDs. returns true if "CTQ" is present in the cell # FDR rules: CTQ (critical to quality) features should be called out in the Design Output features column. # CTQs should be called out using the following format: (CTQ08) def has_ctq_id(value): if value.lower().find("ctq") != -1: return True else: return False # Design Output Features # check for CTQ number after CTQ tag. returns true if all occurrences of CTQ are followed by two set_and_get_funcs # returns false if no CTQs are present OR they are not followed by two set_and_get_funcs. (this should be used in conjunction # with the previous function that looks for CTQ in the cell to eliminate possibility of the former case) # FDR rules: CTQ (critical to quality) features should be called out in the Design Output features column. # CTQs should be called out using the following format: (CTQ08) def has_ctq_numbers(value): ctq_count = 0 number_count = 0 # find get_index of first CTQ ID ctq_index = value.lower().find("ctq") # while loop will keep searching for CTQ IDs until there are none. the string is sliced, checked for set_and_get_funcs, # searched for a new ID, get_index found for new CTQ ID, repeat. while ctq_index != -1: # add 1 to ctq_counter, if there were no CTQs, the while condition would not be met. ctq_count += 1 # slice value_at_index from after "ctq" value = value[ctq_index + 3:] # if the next two characters are numbers (they should be if formatted correctly) if value[0:2].isdigit() is True: # add 1 to number counter. this counter will be compared to ctq_count later. they should match number_count += 1 # search for next CTQ. if there are not, find() will output a -1 and while loop will end ctq_index = value.lower().find("ctq") # if "ctq" and number count match AND they aren't zero...they are formatted correctly. if (ctq_count == number_count) and ctq_count > 0: return True else: return False # Requirement Statement # checks for hash (#) symbol in string # FDR rules: hastags are used to identify parent/child relationships, # functional requirements, mating part requirements, user interface requirements and mechanical properties def has_hash(value): if value.find("#") != -1: return True else: return False # Requirement Statement # checks for #Function in cell. # FDR rules: The requirement statement can be tagged using #Function to identify a functional requirement def has_hash_function(value): if value.find("#Function") != -1: return True else: return False # Requirement Statement # checks for #MatingParts # FDR rules: The requirement statement can be tagged using #MatingParts to identify a requirement pertaining to proper # fitting between components def has_hash_mating_parts(value): if value.find("#MatingParts") != -1: return True else: return False # Requirement Statement # checks for #MechProperties # FDr rules: The requirement statement can be tagged using #MechProperties to identify a requirement that pertains to # the mechanical properties of the implant/instrument def has_hash_mech_properties(value): if value.find("#MechProperties") != -1: return True else: return False # Requirement Statement # checks for #UserInterface # FDR rules: the requirement statement can be tagged using #UserInterface to identify a requirement that relates to how # the user handles the implant/instrument def has_hash_user_interface(value): if value.find("#UserInterface") != -1: return True else: return False # TODO will #Parent or #AdditionalParent be used in requirement statement? sticking with #Parent for now # Requirement Statement # checks for #Child returns true if #Child is present # FDR rules: #Child and #Parent are used to link a Design Input that leads to a Design Output Solution that has # been documented earlier in the form. The Design Input is tagged using #Child = P###-### where the ID refers to the # Output solution and the Output solution is tagged using #Parent = P###-### where the ID refers to the Design Input def has_hash_child(value): if value.find("#Child") != -1: return True else: return False # Requirement Statement # checks for #Parent returns true if #Parent is present # FDR rules: #Child and #Parent are used to link a Design Input that leads to a Design Output Solution that has # been documented earlier in the form. The Design Input is tagged using #Child = P###-### where the ID refers to the # Output solution and the Output solution is tagged using #Parent = P###-### where the ID refers to the Design Input # TODO let's not use the word "hash" here. It has a very specific meaning in computer science which can cause # confusion. In fact, you've probably already heard/seen it mentioned in the git literature. It's synonymous with # "checksum". It's how Python dictionaries work too. def has_hash_parent(value): if value.find("#Parent") != -1: return True else: return False # Requirement Statement # returns IDs (P###-###) that are tagged using #Child as a list. assumes there are #Child present. # FDR rules: #Child and #Parent are used to link a Design Input that leads to a Design Output Solution that has # been documented earlier in the form. The Design Input is tagged using #Child = P###-### where the ID refers to the # Output solution and the Output solution is tagged using #Parent = P###-### where the ID refers to the Design Input def child_ids(value): # init output list. will append with values later ids_output_list = [] # remove spaces for easier evaluation value = value.replace(" ", "") # while there are #child in string. string will be sliced after each ID is retrieved while value.find("#Child") != -1: # find the get_index of the child hashtag hash_index = value.find("#Child") value = value[hash_index:] # find the beginning of the ID by searching for P id_start_index = value.find("P") # append output list with ID ids_output_list.append(value[id_start_index:id_start_index + 7]) value = value[id_start_index:] return ids_output_list # Requirement Statement # returns IDs (P###-###) that are tagged using #Parent as a list. assumes there are #Parent present. # FDR rules: #Child and #Parent are used to link a Design Input that leads to a Design Output Solution that has # been documented earlier in the form. The Design Input is tagged using #Child = P###-### where the ID refers to the # Output solution and the Output solution is tagged using #Parent = P###-### where the ID refers to the Design Input def parent_ids(value): # init output list. will append with values later ids_output_list = [] # remove spaces for easier evaluation value = value.replace(" ", "") # while there are #child in string. string will be sliced after each ID is retrieved while value.find("#Parent") != -1: # find the get_index of the child hashtag hash_index = value.find("#Parent") # slice value_at_index from the hash_index + 2 (to account for capital P at the beginning of Parent) to the end value = value[hash_index+2:] # find the beginning of the ID by searching for P id_start_index = value.find("P") # append output list with ID ids_output_list.append(value[id_start_index:id_start_index + 7]) value = value[id_start_index:] return ids_output_list #list of tags """ SANDBOX """ if __name__ == '__main__': # This is your playground # call function # print result testval = "blah blah TBD \n anatomy (TBD percentile, etc.). \nFunction #Parent = P40-030 #Parent = P40-040" testout = remove_carriage_returns(testval) print(testout) pass """ # init a mock requirement (row on FDR) for testing req1 = dict(iD="P20", procedureStep=" ", userNeed="X", cascadeLevel="DESIGN OUTPUT SOLUTION", requirementStatement="Prepare Patient") print("\n") print(req1) print("\n") """ PK OH) rtm/_old/headers_list.py""" this script initializes a list of "approved" column headers. It also defines a function that compares the approved list to an input list (future state will be from User's file) and returns a named tuple with found, not found and extra fs. """ """ #USE THIS!! list1 = [ 'item1', 'item2', ] """ def header_check(approved_headers_list,users_headers_list): # use set() to determine what is missing from users list (approved - users) missing_from_users = list(set(approved_headers_list)-set(users_headers_list)) # use set() to determine the extra items on users list (users - approved) users_extra = list(set(users_headers_list)-set(approved_headers_list)) if (missing_from_users and users_extra) == []: matches = users_headers_list """ SANDBOX """ if __name__ == '__main__': # approved_headers_list contains the "approved" column headers approved_headers_list = [] approved_headers_list.append("ID") approved_headers_list.append("Procedure Step") approved_headers_list.append("User Need") approved_headers_list.append("Design Input") approved_headers_list.append("DO Solution L1") approved_headers_list.append("DO Solution L2") approved_headers_list.append("DO Solution L3") approved_headers_list.append("Cascade Level") approved_headers_list.append("Requirement Statement") approved_headers_list.append("Requirement Rationale") approved_headers_list.append("Verification or Validation Strategy") approved_headers_list.append("Verification or Validation Results") approved_headers_list.append("Devices") approved_headers_list.append("Design Output Feature (with CTQ ID #)") approved_headers_list.append("CTQ? Yes, No, N/A") # print(approved_headers_list) # headers_list contains a sample users column headers users_headers_list = [] users_headers_list.append("ID") users_headers_list.append("Procedure Step") users_headers_list.append("User Need") users_headers_list.append("Design Input") users_headers_list.append("DO Solution L1") users_headers_list.append("DO Solution L2") users_headers_list.append("DO Solution L3") users_headers_list.append("Cascade Level") users_headers_list.append("Requirement Statement") users_headers_list.append("Requirement Rationale") users_headers_list.append("Verification or Validation Strategy") users_headers_list.append("Verification or Validation Results") users_headers_list.append("Devices") users_headers_list.append("Design Output Feature (with CTQ ID #)") users_headers_list.append("CTQ? Yes, No, N/A") users_headers_list.append("Extra Header1") # users_headers_list.append("Extra Header2") # print(users_headers_list) # This is your playground # call function # print result testout = header_check(approved_headers_list,users_headers_list) print(testout) pass PK O.LQQ!rtm/_old/some_random_functions.pyimport openpyxl import pathlib def is_integer(string): integers = str(1234567890) if string in integers: return True else: return False def get_first_integer_sequence(value): """Search through string, return first set of consecutive numbers as single integer :param value: :return: integer. `0` if no integer was found """ first_integer_set_found = False integer_string = '' output = -1 # default try: for char in value: if is_integer(char): # print(char) first_integer_set_found = True integer_string += char elif first_integer_set_found: break else: pass if integer_string != '': output = int(integer_string) finally: return output def convert_to_string_with_leading_zeroes(value, min_length=0) -> str: """ `84, 3` --> `084` `321, 1` --> `321` `hello, 3` --> `hello` :param value: :param min_length: :return: """ output = '0' * min_length # default output try: input_value = str(value) length = len(input_value) # print(f'length = {length}') missing_length = min_length - length if missing_length > 0: leading_zeroes = '0' * missing_length # print(f'leading zeroes = {leading_zeroes}') output = leading_zeroes + input_value else: output = input_value finally: return output PK Obí rtm/containers/field.py# --- Standard Library Imports ------------------------------------------------ import abc from typing import List # --- Third Party Imports ----------------------------------------------------- # None # --- Intra-Package Imports --------------------------------------------------- import rtm.main.context_managers as context import rtm.validate.validation as val from rtm.containers.worksheet_columns import get_matching_worksheet_columns from rtm.validate import validator_output class Field(): def __init__(self, name): self._name = name # --- Get matching columns -------------------------------------------- matching_worksheet_columns = get_matching_worksheet_columns( context.worksheet_columns.get(), self.get_name() ) # --- Set Defaults ---------------------------------------------------- self._indices = None # Used in later check of relative column position self._body = None # column data self._correct_position = None # Checked during the Validation step self._val_results = None # --- Override defaults if matches are found -------------------------- if len(matching_worksheet_columns) >= 1: # Get all matching indices (used for checking duplicate data and proper sorting) self._indices = [col.index for col in matching_worksheet_columns] # Get first matching column data (any duplicate columns are ignored; user rcv warning) self._body = matching_worksheet_columns[0].body def validate(self) -> None: # --- Was the field found? -------------------------------------------- field_found = self.field_found() # --- Generate the minimum output message ----------------------------- self._val_results = [ validator_output.OutputHeader(self.get_name()), # Start with header val.val_column_exist(field_found), ] # --- Perform remaining validation if the field was found ------------- if field_found: self._val_results.append(val.val_column_sort(self)) self._val_results += self._validation_specific_to_this_field() def _validation_specific_to_this_field(self) -> List[validator_output.ValidatorOutput]: return validator_output.example_val_results() def print(self): for result in self._val_results: result.print() def field_found(self): if self._body is None: return False return True def get_index(self): return self._indices[0] def get_body(self): return self._body def get_name(self): return self._name def get_min_index_for_field_right(self): return self.get_index() def __str__(self): return self.__class__, self.field_found() if __name__ == "__main__": pass PK O~ rtm/containers/fields.py# --- Standard Library Imports ------------------------------------------------ import collections from typing import List # --- Third Party Imports ----------------------------------------------------- import click # --- Intra-Package Imports --------------------------------------------------- import rtm.containers.field as ft import rtm.main.context_managers as context import rtm.validate.validation as val from rtm.validate.validator_output import ValidationResult, OutputHeader class Fields(collections.abc.Sequence): # --- Class handling ------------------------------------------------------ _field_classes = [] @classmethod def get_field_classes(cls): return cls._field_classes @classmethod def append_field(cls, field_class): # if not issubclass(field_class, Field): # raise TypeError cls._field_classes.append(field_class) @classmethod def collect_field(cls, collect=True): def decorator(field_): if collect: # This is so I can easily switch off the collection of a field cls.append_field(field_) return field_ return decorator # --- Object handling ----------------------------------------------------- def __init__(self): self.body_length = context.worksheet_columns.get().body_length self._fields = [field_class() for field_class in self.get_field_classes()] def get_matching_field(self, field_class): if isinstance(field_class, str): for _field in self: if _field.__class__.__name__ == field_class: return _field else: for _field in self: if isinstance(_field, field_class): return _field raise ValueError(f'{field_class} not found in {self.__class__}') # --- Sequence ------------------------------------------------------------ def __getitem__(self, item): return self._fields[item] def __len__(self) -> int: return len(self._fields) def validate(self): click.echo(self) for field_ in self: click.echo(field_.get_name()) field_.validate() def print(self): click.echo(self) for field_ in self: field_.print() @Fields.collect_field() class ID(ft.Field): def __init__(self): super().__init__("ID") def _validation_specific_to_this_field(self) -> List[ValidationResult]: results = [ val.val_cells_not_empty(self._body), ] return results @Fields.collect_field() class CascadeBlock(ft.Field): def __init__(self): self._subfields = [] for subfield_name in self._get_subfield_names(): subfield = CascadeSubfield(subfield_name) if subfield.field_found(): self._subfields.append(subfield) else: break @staticmethod def _get_subfield_names(): field_names = ["Procedure Step", "User Need", "Design Input"] for i in range(1, 20): field_names.append("DO Solution L" + str(i)) return field_names def validate(self): """ if index=0, level must == 0. If not, error """ work_items = context.work_items.get() validation_outputs = [ OutputHeader(self.get_name()), val.val_cascade_block_only_one_entry(work_items), val.val_cascade_block_x_or_f(work_items), val.val_cascade_block_use_all_columns() ] for output in validation_outputs: output.print() def print(self): # TODO click.echo("The Cascade Block isn't printing anything useful yet.") def field_found(self): if len(self) > 0: return True else: return False def get_body(self): return [subfield.get_body() for subfield in self] def get_min_index_for_field_right(self): if self.field_found(): return self[-1].get_index() else: return None def get_name(self): return 'Cascade Block' def get_index(self): if self.field_found(): return self[0].get_index() else: return None def __len__(self): return len(self._subfields) def __getitem__(self, item): return self._subfields[item] # Not a collected field; rolls up under CascadeBlock class CascadeSubfield(ft.Field): def __init__(self, subfield_name): super().__init__(subfield_name) def get_name(self): return self._name @Fields.collect_field() class CascadeLevel(ft.Field): def __init__(self): super().__init__("Cascade Level") # def _validation_specific_to_this_field(self) -> List[ValidationResult]: # return val.example_results() @Fields.collect_field() class ReqStatement(ft.Field): def __init__(self): super().__init__("Requirement Statement") # def _validation_specific_to_this_field(self) -> List[ValidationResult]: # return val.example_results() @Fields.collect_field() class ReqRationale(ft.Field): def __init__(self): super().__init__("Requirement Rationale") # def _validation_specific_to_this_field(self) -> List[ValidationResult]: # return [val.val_cells_not_empty(self._body)] @Fields.collect_field() class VVStrategy(ft.Field): def __init__(self): super().__init__("Verification or Validation Strategy") # def _validation_specific_to_this_field(self) -> List[ValidationResult]: # return val.example_results() @Fields.collect_field() class VVResults(ft.Field): def __init__(self): super().__init__("Verification or Validation Results") # def _validation_specific_to_this_field(self) -> List[ValidationResult]: # return [] @Fields.collect_field() class Devices(ft.Field): def __init__(self): super().__init__("Devices") def _validation_specific_to_this_field(self) -> List[ValidationResult]: return [val.val_cells_not_empty(self._body)] @Fields.collect_field() class DOFeatures(ft.Field): def __init__(self): super().__init__("Design Output Feature (with CTQ ID #)") def _validation_specific_to_this_field(self) -> List[ValidationResult]: return [] @Fields.collect_field() class CTQ(ft.Field): def __init__(self): super().__init__("CTQ? Yes, No, N/A") def _validation_specific_to_this_field(self) -> List[ValidationResult]: return [] if __name__ == "__main__": for field in Fields.get_field_classes(): print(field) PK Ortm/containers/work_items.py# --- Standard Library Imports ------------------------------------------------ import collections from typing import List # --- Third Party Imports ----------------------------------------------------- # None # --- Intra-Package Imports --------------------------------------------------- from rtm.containers.fields import CascadeBlock import rtm.main.context_managers as context from rtm.main.exceptions import UninitializedError from rtm.validate.checks import cell_empty class WorkItem: def __init__(self, index_): self.index = index_ # work item's vertical position relative to other work items # contrast with position, which is which cascade column is marked self.cascade_block_contents = OrderedDictList() self._parent = UninitializedError() def has_parent(self): if self.parent is None: return False elif self.parent >= 0: return True else: return False def set_cascade_block_row(self, cascade_block_row: list): for index_, value_ in enumerate(cascade_block_row): if not cell_empty(value_): self.cascade_block_contents[index_] = value_ @property def position(self): try: return self.cascade_block_contents.get_first_key() except IndexError: return None @property def parent(self): return self._parent @parent.setter def parent(self, value_): self._parent = value_ def find_parent(self, work_items): # set default self.parent = None if self.position is None: # If no position (row was blank), then no parent return elif self.position == 0: # If in first position, then it's the trunk of a tree! self.parent = -1 return # Search back through previous work items for index_ in reversed(range(self.index)): other = work_items[index_] if other.position is None: # Skip work items that have a blank cascade. Keep looking. continue elif other.position == self.position: # same position, same parent self.parent = other.parent return elif other.position == self.position - 1: # one column to the left; that work item IS the parent self.parent = other.index return elif other.position < self.position - 1: # cur_work_item is too far to the left. There's a gap in the chain. No parent return else: # self.position < other.position # Skip work items that come later in the cascade. Keep looking. continue class WorkItems(collections.abc.Sequence): def __init__(self): # --- Get Cascade Block ----------------------------------------------- fields = context.fields.get() cascade_block = fields.get_matching_field(CascadeBlock) cascade_block_body = cascade_block.get_body() # --- Initialize Work Items ------------------------------------------- self._work_items = [WorkItem(index_) for index_ in range(fields.body_length)] for work_item in self: row_data = get_row(cascade_block_body, work_item.index) work_item.set_cascade_block_row(row_data) work_item.find_parent(self._work_items) def __getitem__(self, item) -> WorkItem: return self._work_items[item] def __len__(self) -> int: return len(self._work_items) class OrderedDictList(collections.OrderedDict): def value_at_index(self, index_: int): try: return list(self.values())[index_] except IndexError: raise IndexError("OrderedDictList index_ out of range") def get_first_key(self): try: return list(self.keys())[0] except IndexError: return None def get_first_value(self): first_key = self.get_first_key() if first_key is None: return None return self[first_key] def get_row(columns: List[list], index_: int) -> list: return [col[index_] for col in columns] if __name__ == "__main__": pass PK OuM8 8 #rtm/containers/worksheet_columns.py# --- Standard Library Imports ------------------------------------------------ from collections import namedtuple from typing import List # --- Third Party Imports ----------------------------------------------------- import openpyxl # --- Intra-Package Imports --------------------------------------------------- from rtm.main.exceptions import RTMValidatorFileError import rtm.main.context_managers as context WorksheetColumn = namedtuple("WorksheetColumn", "header body index column") class WorksheetColumns: def __init__(self, worksheet_name): # --- Attributes ------------------------------------------------------ self.max_row = None self.cols = None self.body_length = 0 # --- Get Path ---------------------------------------------------- path = context.path.get() # --- Get Workbook ------------------------------------------------ wb = openpyxl.load_workbook(filename=str(path), read_only=True, data_only=True) # --- Get Worksheet ----------------------------------------------- ws = None for sheetname in wb.sheetnames: if sheetname.lower() == worksheet_name.lower(): ws = wb[sheetname] if ws is None: raise RTMValidatorFileError( f"\nError: Workbook does not contain a '{worksheet_name}' worksheet" ) self.max_row = ws.max_row self.body_length = self.max_row - 1 # --- Convert Worksheet to WorksheetColumn objects ---------------- ws_data = [] start_column_num = 1 for index, col in enumerate(range(start_column_num, ws.max_column + 1)): column_header = ws.cell(1, col).value column_body = tuple(ws.cell(row, col).value for row in range(2, self.max_row + 1)) ws_column = WorksheetColumn( header=column_header, body=column_body, index=index, column=col ) ws_data.append(ws_column) self.cols = ws_data def __getitem__(self, index): return self.cols[index] def __len__(self): return len(self.cols) 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 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 PK Ogrtm/main/api.py# --- Standard Library Imports ------------------------------------------------ # None # --- Third Party Imports ----------------------------------------------------- import click # --- Intra-Package Imports --------------------------------------------------- # TODO consolidate imports from rtm.main.excel import get_rtm_path 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 def main(): click.clear() click.echo( "\nWelcome to the DePuy Synthes Requirements Trace Matrix (RTM) Validator." "\nPlease select an RTM excel file you wish to validate." ) try: with context.path.set(get_rtm_path()): worksheet_columns = wc.WorksheetColumns("Procedure Based Requirements") 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() 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." ) if __name__ == "__main__": main() PK OACZZrtm/main/cli.py# --- Standard Library Imports ------------------------------------------------ # None # --- Third Party Imports ----------------------------------------------------- import click # --- Intra-Package Imports --------------------------------------------------- from rtm.main import api @click.command() def main(): api.main() PK OI+rtm/main/context_managers.py# --- Standard Library Imports ------------------------------------------------ from contextlib import contextmanager # --- Third Party Imports ----------------------------------------------------- # None # --- Intra-Package Imports --------------------------------------------------- from rtm.main.exceptions import UninitializedError class ContextManager: def __init__(self, name): """Easily share data amongst all the functions""" self._name = name self._value = None @contextmanager def set(self, value): self._value = value yield self._value = None def get(self): if self._value is None: raise UninitializedError(f"The '{self._name}' ContextManager is not initialized") else: return self._value path = ContextManager('path') worksheet_columns = ContextManager('worksheet_columns') fields = ContextManager('fields') work_items = ContextManager('work_items') PK Olrtm/main/excel.pyimport tkinter as tk from pathlib import Path from tkinter import filedialog import click from rtm.main import exceptions as exc def get_rtm_path(path_option='default') -> Path: 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: root = tk.Tk() root.withdraw() path = Path(filedialog.askopenfilename()) return pathPK OL&>rtm/main/exceptions.py# --- 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 class Uninitialized(RTMValidatorError): pass PK Owr]rtm/validate/checks.py""" Whereas the validation functions return results that will be outputted by the RTM Validator, these "check" functions perform smaller tasks, like checking individual cells. """ # --- Standard Library Imports ------------------------------------------------ from typing import Optional # --- Third Party Imports ----------------------------------------------------- # None # --- Intra-Package Imports --------------------------------------------------- import rtm.main.context_managers as context def cell_empty(value) -> bool: # If the cell contained True or False, then clearly it wasn't empty. Return False if isinstance(value, bool): return False if not value: return True return False 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] def dict_contains_only_acceptable_entries(dictionary, acceptible_entries): if len(dictionary) == 0: return True for value in dictionary.values(): if value not in acceptible_entries: return False return True PK O&rtm/validate/validation.py""" These functions each check a specific aspect of an RTM field and return a ValidationResult object, ready to be printed on the terminal as the final output of this app. """ # --- Standard Library Imports ------------------------------------------------ # None # --- Third Party Imports ----------------------------------------------------- # None # --- Intra-Package Imports --------------------------------------------------- import rtm.validate.checks as check from rtm.validate.validator_output import ValidationResult import rtm.main.context_managers as context def val_column_exist(field_found) -> ValidationResult: title = "Field Exist" if field_found: score = "Pass" explanation = None else: score = "Error" explanation = "Field not found" return ValidationResult(score, title, explanation) def val_column_sort(field) -> ValidationResult: """Does the argument field actually appear after the one it's supposed to?""" title = "Left/Right Order" field_left = check.get_expected_field_left(field) 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.get_min_index_for_field_right() <= field.get_index(): # argument field is to the right of its expected left-hand neighbor score = "Pass" explanation = ( f"This field comes after the {field_left.get_name()} field as it should" ) else: score = "Error" explanation = f"This field should come after {field_left.get_name()}" return ValidationResult(score, title, explanation) def val_cells_not_empty(values) -> ValidationResult: title = "Not Empty" indices = [] for index, value in enumerate(values): if check.cell_empty(value): indices.append(index) if not indices: score = "Pass" explanation = "All cells are non-blank" else: score = "Error" explanation = "Action Required. The following rows are blank:" return ValidationResult(score, title, explanation, indices) def val_cascade_block_only_one_entry(work_items): title = "Single Entry" indices = [] for work_item in work_items: _len = len(work_item.cascade_block_contents) if _len != 1: indices.append(work_item.index) if not indices: score = "Pass" explanation = "All rows have a single entry" else: score = "Error" explanation = ( "Action Required. The following rows are blank or have multiple entries:" ) return ValidationResult(score, title, explanation, indices) def val_cascade_block_x_or_f(work_items) -> ValidationResult: """Value in first position must be X or F.""" title = "X or F" acceptable_entries = "X F".split() error_indices = [ index for index, work_item in enumerate(work_items) if not check.dict_contains_only_acceptable_entries( work_item.cascade_block_contents, acceptable_entries ) ] if not error_indices: score = "Pass" explanation = f"All entries are one of {acceptable_entries}" else: score = "Error" explanation = f"Action Required. The following rows contain something other than the allowed {acceptable_entries}:" return ValidationResult(score, title, explanation, error_indices) def val_cascade_block_use_all_columns(): title = "Use All Columns" # Setup fields fields = context.fields.get() cascade_block = fields.get_matching_field('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.cascade_block_contents.get_first_key() for work_item in work_items ) missing_positions = positions_expected - positions_actual 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) def get_row(index): return index + 2 # TODO: replace with something more maintainable / straightforward cell_validation_functions = [ globals()[name] for name in globals() if name.startswith("val_cells_") ] if __name__ == "__main__": print(cell_validation_functions) PK O:r 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 typing import List # --- Third Party Imports ----------------------------------------------------- import click # --- Intra-Package Imports --------------------------------------------------- # None class ValidatorOutput(metaclass=abc.ABCMeta): @abc.abstractmethod def print(self): return class ValidationResult(ValidatorOutput): def __init__(self, score, title, explanation=None, nonconforming_indices=None): self._scores_and_colors = {'Pass': 'green', 'Warning': 'yellow', 'Error': 'red'} self._set_score(score) self._title = title self._explanation = explanation self._set_indices(nonconforming_indices) def _set_indices(self, indices: List[int]): if indices: self.indices = tuple(indices) else: self.indices = '' def _set_score(self, score) -> None: if score not in self._scores_and_colors: raise ValueError(f'{score} is an invalid score') self._score = score def _get_color(self) -> str: return self._scores_and_colors[self._score] def _get_rows(self) -> str: if not self.indices: return '' first_row = 2 # this is the row # directly after the headers rows = (index + first_row for index in self.indices) return ' ' + str(rows) 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}{self.indices}', nl=False) click.echo() # new line class OutputHeader(ValidatorOutput): 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() def example_val_results() -> List[ValidationResult]: explanation = 'This is an example explanation' examples = [ ValidationResult('Pass', 'Pass Example', explanation), ValidationResult('Warning', 'Warning Example', explanation), ValidationResult('Error', 'Error Example', explanation), ] return examplesPK!H)YD'))dps_rtm-0.1.14.dist-info/entry_points.txtN+I/N.,()**ɵb~uQHklV$iU*;+EI|0*ʍåY=S 2,~ХtEj'a__#خvy;B|D-60ݬIݏX#ҔqGw >zç:/.s^hmL4.ʚJ䮛޺kP烦Re~_,6B>}"I Q],ci.fX+=\1[-KT~U&6vc[`%:Gtz:)yNMeLSlyCl5cUL R0]I&h([*O*pZ٘~w erR Թ_# mq׏VIdtrq\P>咜.t.-Z\)i3 2'Ypijv*ƢpjHa |\R-VcC'%S.SWayh&"u cÖnCS`:3|fcW9C0ĊC JI +]ޘ |0VՁSmЖ_/p`:X*]um:Y 2dA!@AYk,6u؅^QAq}J8gYWGIS68x,L]z2 Y]H<< ,/7b+<,mdcbTN Vn*(&z . w4Y++Jeer""jd DCC,  #1<"$t.8(3p^݃C =d#:N vtavlfva @su-T?\_WehaHr1!=NWc&^sqxtoV&4l׮P̀a6d 8n霞;nmvӘZ2:tf]w1O&ӋO7;'R?tKtkZ@QZUAƿ3VR0\L6=^gK/quSEnd+۠J閷ngd?uém!]ܘz%,\)bp/p@mЖUhT'chz#\\J)͂dUn%8 9xF=O%UţRU`Q:`: &Bo5]aZlhmb.ʲ˻܀?i2.8_'c'րiAք6oNtcؤvk_ѱ3t49^U]ߣ㠨~\mš&}Xt7S6a!y ^6jc BPK!Hn2Gdps_rtm-0.1.14.dist-info/RECORD}I{H-0^`U@T0@2˯o1Ijw:m_a> _;]"ah:up՟R3!"^O.r=epE(bKAU\@Rj0`vOl]ܷv{8za*|HVٕ1iu@tP#q"? RvaUtXp3U*Q@'4BE܁/qqP 6)>kxxJCa<[^I8-KBJm& #X-'QA_T'̑-^!PXҩz)wm\]@Xq/|=ȌDxMkmYMxj^z c?'ԖaуhkIs^ vP'㙛S9d4-]UCEO&*f qհ`L]i<FMmM@tMp3TR)yg`NR=;ޖ8\Dp4xYsy0fG`:Q쎫xS}Lvd7zqs'(3YUʁFiE5Qz|-jcK}O3͖:RY.ms!j'9N*snjUV`߇G'|':NmiF|۩Y@)0bnoc43ߒ 7󆀸fTl%**a1Q*H l VJgdž_iӅ|/>Ϊzoa w/0FJ 6}i+Fyiklt\MZٺ;zUࠜb"_Z@nlvj'"ڜ1ᓪ(KNQBM |ƺUkq{m-k('ZAŃO\!MJ(Kb!^$?lB2o>6/F3Ȏ92PK O0@JJrtm/__init__.pyPK Oī@N@N wrtm/_old/check_form_functions.pyPK OH) Nrtm/_old/headers_list.pyPK O.LQQ!Zrtm/_old/some_random_functions.pyPK Obí |artm/containers/field.pyPK O~ ^mrtm/containers/fields.pyPK Ortm/containers/work_items.pyPK OuM8 8 #nrtm/containers/worksheet_columns.pyPK Ogrtm/main/api.pyPK OACZZrtm/main/cli.pyPK OI+rtm/main/context_managers.pyPK Olrtm/main/excel.pyPK OL&>rtm/main/exceptions.pyPK Owr]mrtm/validate/checks.pyPK O&rtm/validate/validation.pyPK O:r rtm/validate/validator_output.pyPK!H)YD'))dps_rtm-0.1.14.dist-info/entry_points.txtPKBNuQQ !dps_rtm-0.1.14.dist-info/LICENSEPK!HPOdps_rtm-0.1.14.dist-info/WHEELPK!HnQk!<dps_rtm-0.1.14.dist-info/METADATAPK!Hn2G dps_rtm-0.1.14.dist-info/RECORDPKD