PKr0OdEErtm/__init__.py""" Validate a Requirements Trace Matrix """ __version__ = "0.1.16" PKr0Oԧjjrtm/_old/rule_functions.py# """ # # This script serves as a library of functions for FDR validator. These functions are derived from the Validator Rules # outlined in fdr.docs.README. These functions will be called in validate.validation to analyze the content of specific # cells and entire columns. Rules that relate to cell/column/row relationships are maintained in XXXXXXX.XXXXXXXX. # # Each function has 6 comments before it, the first is which field it is intended for (ID, cascade block, requirement # statement, etc. ) the second states the type of input, the third states the type of output, the fourth is # the Validator Rule (from README) that it is intended to apply, the fifth is a description of it's functionality, # the sixth is the status of testing (see unit.test_validate.test_rule_functions). # # # Field: ID, Devices, etc. # # Input: string, list, etc. # # Output: Boolean, string, list # # Validator Rule: use similar verbiage as README for traceability # # Description: describe in english what the function does # # Test Status: Incomplete, In-progress, complete # def function() # function body goes here... # # """ # # # # Field: ID # # Input: String # # Output: Boolean # # Validator Rule: 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 # # Description: check that input has a capital P as the first character. # # Test Status: Complete # def starts_with_p(value): # if value.startswith("P"): # return True # else: # return False # # # # Field: ID (procedure steps only) # # Input: string # # Output: boolean # # Validator Rule: recommended ID formatting for procedure steps follow a naming convention. # # # e.g. P010, P020, etc. for procedure steps # # Description: slices string to ignore first character then checks if the remaining characters are integers # # Test Status: Complete # def has_only_digits_after_first(value): # return value[1:].isdigit() # # # # Field: ID (procedure steps only) # # Input: string # # Output: boolean # # Validator Rule: recommended ID formatting for procedure steps follow a naming convention. # # # e.g. P010, P020, etc. for procedure steps # # Description: check if string has 3 integers following the first character. First character is omitted # # Test Status: Complete # def has_only_three_digits(value): # value_sliced = value[1:] # if (len(value_sliced) == 3) and (value_sliced.isdigit() is True): # return True # else: # return False # # # # Field: ID (non-procedure steps aka need, input, output, etc.) # # Input: string # # Output: boolean # # Validator rule: recommended ID formatting for requirements follow a naming convention. # # e.g. P010-020, P010-030, etc. # # Description: check if value_at_index has 6 integers following the first character. First char is omitted. Assumes # # there is a dash and removes it # # Test Status: Complete # def has_only_six_digits(value): # # slice string. keep all characters after the first. (removes P) # value_slice = value[1:] # # removes hyphen from string # value_slice = value_slice.replace("-", "") # if (len(value_slice) == 6) and (value_slice.isdigit() is True): # return True # else: # return False # # # # Field: ID # # Input: string # # Output: boolean # # Validator rule: recommended ID formatting for requirements follow a naming convention. # # e.g. P010-020, P010-030, etc. # # Description: check for hyphen within string, location and number of occurrences don't matter # # Test Status: Complete # def has_hyphen(value): # if value.find("-") != -1: # return True # else: # return False # # # # Field: ID # # Input: string # # Output: boolean # # Validator rule: recommended ID formatting for requirements follow a naming convention. # # e.g. P010-020, P010-030, etc. # # Description: check for hyphen in 4th position within string. ex. "P010-010" by looking for lowest and highest index # # of hyphen using find() and rfind() respectively and comparing them to expected 4th position. # # Test Status: Complete # def has_single_hyphen_positioned(value): # if (value.find("-") == 4) and (value.rfind("-") == 4): # return True # else: # return False # # # # Field: Cascade block # # Input: string # # Output: boolean # # Validator Rule: only a capital X or capital F are allowed in the cascade visualizer columns. (B-G in its current form) # # Description: remove spaces and check for capital X. returns false if any other character is present # # Test Status: Incomplete # def is_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 # # # # Field: Cascade block # # Input: string # # Output: boolean # # Validator Rule: only a capital X or capital F are allowed in the cascade visualizer columns. (B-G in its current form) # # Description: remove spaces and check for lower case X. returns false if any other character is present # # Test Status: Incomplete # def is_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 # # # # Field: Cascade block # # Input: string # # Output: boolean # # Validator Rule: only a capital X or capital F are allowed in the cascade visualizer columns. (B-G in its current form) # # Description: remove spaces and check for capital F. returns false if any other character is present # # Test Status: Incomplete # def is_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 # # # # Field: Cascade block # # Input: string # # Output: boolean # # Validator Rule: only a capital X or capital F are allowed in the cascade visualizer columns. (B-G in its current form) # # Description: remove spaces and check for lower case f. returns false if any other character is present # # Test Status: Incomplete # def is_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 # # # # Field: Cascade level # # Input: string # # Output: boolean # # Validator Rule: 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 # # Description: check if cascade level is 'procedure step' by removing white space at the ends and changing all # # characters to lowercase for direct string comparison # # Test Status: Incomplete # def is_procedure_step(value): # # remove whitespace at the beginning and end of the string and convert to lower case # if value.strip().lower() == "procedure step": # return True # else: # return False # # # # Field: Cascade level # # Input: string # # Output: boolean # # Validator Rule: 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 # # Description: check if cascade level is 'user need' by removing white space at the ends and changing all # # characters to lowercase for direct string comparison # # Test Status: Incomplete # 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 # # # # Field: Cascade level # # Input: string # # Output: boolean # # Validator Rule: 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 # # Description: check if cascade level is 'risk need' by removing white space at the ends and changing all # # characters to lowercase for direct string comparison # # Test Status: Incomplete # 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 # # # # Field: Cascade level # # Input: string # # Output: boolean # # Validator Rule: 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 # # Description: check if cascade level is 'business need' by removing white space at the ends and changing all # # characters to lowercase for direct string comparison # # Test Status: Incomplete # 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 # # # # Field: Cascade level # # Input: string # # Output: boolean # # Validator Rule: 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 # # Description: check if cascade level is 'design input' by removing white space at the ends and changing all # # characters to lowercase for direct string comparison # # Test Status: Incomplete # 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 # # # # Field: Cascade level # # Input: string # # Output: boolean # # Validator Rule: 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 # # Description: check if cascade level is 'design output' by removing white space at the ends and changing all # # characters to lowercase for direct string comparison # # Test Status: Incomplete # 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": # return True # else: # return False # # # # Field: Cascade level # # Input: string # # Output: boolean # # Validator Rule: cascade level may only be one of the 6 defined types. # # # procedure step, user need, risk need, business need, design input or design output # # Description: check if cascade level is one of the approved options by running all previously defined functions # # that look for cascade level. if only 1 is true, the cascade level cell in that row contains one of the approved values # # Test Status: Incomplete # def is_cascade_lvl_approved(value): # cascade_list = [ # is_procedure_step(value), # is_user_need(value), # is_risk_need(value), # is_business_need(value), # is_design_input(value), # is_design_output(value), # ] # if cascade_list.count(True) == 1: # return True # else: # return False # # # # Field: Requirement Statement # # Input: string # # Output: boolean # # Validator Rules: hastags are used to identify parent/child relationships, # # # functional requirements, mating part requirements, user interface requirements and mechanical properties # # Description: looks for pound/number symbol in string. returns true if present and not followed by numbers (this is to # # differentiate relationship hashtag from a windchill number) # # Test Status: Complete # def has_hashtag(value): # pound_indices = [] # hashtag_list = [] # if value.find("#") == -1: # return False # else: # for index, char in enumerate(value): # if char == "#": # pound_indices.append(index) # for test_idx in pound_indices: # hashtag_list.append(value[test_idx + 1:test_idx + 4].isalpha()) # if any(hashtag_list) is True: # return True # else: # return False # # # # Field: Requirement Statement # # Input: string # # Output: integer (occurrences) # # Validator Rules: hastags are used to identify parent/child relationships, # # # functional requirements, mating part requirements, user interface requirements and mechanical properties # # Description: looks for pound/number symbol in string. counts pound symbols that are followed by 3 letters (this is to # # differentiate relationship hashtag from a windchill number) # # Test Status: Complete # def count_hashtags(value): # pound_indices = [] # hashtag_list = [] # if value.find("#") == -1: # return 0 # else: # for index, char in enumerate(value): # if char == "#": # pound_indices.append(index) # for test_idx in pound_indices: # hashtag_list.append(value[test_idx + 1:test_idx + 4].isalpha()) # return hashtag_list.count(True) # # # # Field: Requirement Statement # # Input: string # # Output: boolean # # Validator rules: The requirement statement can be tagged using #Function to identify a functional requirement # # Description: checks for #Function in cell by converting to lower case and using direct string comparison. # # Test Status: Complete # def has_hashtag_function(value): # if value.lower().find("#function") != -1: # return True # else: # return False # # # # Field: Requirement Statement # # Input: string # # Output: boolean # # Validator rules: The requirement statement can be tagged using #MatingParts to identify a requirement pertaining to # # proper fitting between components # # Description: checks for #MatingParts by converting to lower case and using direct string comparison. # # Test Status: Complete # def has_hashtag_mating_parts(value): # if value.lower().find("#matingparts") != -1: # return True # else: # return False # # # ##TODO Bookmark1 - info below # """ # BOOKMARK1 - Above this line, all functions have been tested in test_rule_functions and have uniform comments that relate # to README. next actions are to continue to revise functions, write uniform comments and resume testing. # """ # # # # Field: Requirement Statement # # Input: string # # Output: boolean # # Validator rules: The requirement statement can be tagged using #MechProperties to identify a requirement that # # pertains to the mechanical properties of the implant/instrument # # Description: checks for #MechProperties by converting to lower case and using direct string comparison. accepts either # # #mechproperties or #mechanicalproperties # # Test Status: Incomplete # def has_hashtag_mech_properties(value): # if value.lower().find("#mechproperties") != -1: # return True # elif value.lower().find("#mechanicalproperties") != -1: # return True # else: # return False # # # # Field: Requirement Statement # # Input: string # # Output: boolean # # Validator rules: the requirement statement can be tagged using #UserInterface to identify a requirement that relates # # to how the user handles the implant/instrument # # Description: checks for #UserInterface by converting to lower case and using direct string comparison # # Test Status: Incomplete # def has_hashtag_user_interface(value): # if value.lower().find("#userinterface") != -1: # return True # else: # return False # # # # Field: Requirement Statement # # Input: string # # Output: boolean # # Validator 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 # # Description: checks for #Child returns true if #Child is present by converting to lower case and using direct string # # comparison # # Test Status: Incomplete # def has_hashtag_child(value): # if value.lower().find("#child") != -1: # return True # else: # return False # # # # FDR # # Field: Requirement Statement # # Input: string # # Output: boolean # # Validator 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 # # Description: checks for #Parent returns true if #Parent is present by converting to lower case and using direct # # string comparison # # Test Status: Incomplete # def has_hashtag_parent(value): # if value.lower().find("#Parent") != -1: # return True # else: # return False # # # # Field: Requirement Statement # # Input: string # # Output: list of strings # # Validator 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 # # Description: returns IDs (P###-###) that are tagged using #Child as a list. assumes there are #Child present. # # Test Status: Incomplete # def retrieve_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.lower().find("#child") != -1: # # find the index of the #child hashtag (pound symbol) # pound_index = value.lower().find("#child") # value = value[pound_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 # # # # Field: Requirement Statement # # Input: string # # Output: list of strings # # Validator 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 # # Description: returns IDs (P###-###) that are tagged using #Parent as a list. assumes there are #Parent present. # # Test Status: Incomplete # def retrieve_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.lower().find("#parent") != -1: # # find the get_index of the child hashtag # hash_index = value.lower().find("#parent") # # slice value_at_index from the hash_index + 2 (to account for "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 # # # ##TODO Bookmark2 - see info below # """ # between bookmarks 1 and 2, comments are revised and uniform. once testing is complete, replace bookmark2 with bookmark1. # """ # # # # 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 # # Field: # # Input: # # Output: # # Description: # # Test Status: Incomplete, In-progress, complete # 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 # # Field: # # Input: # # Output: # # Description: # # Test Status: Incomplete, In-progress, complete # 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) # # Field: # # Input: # # Output: # # Description: # # Test Status: Incomplete, In-progress, complete # 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) # # Field: # # Input: # # Output: # # Description: # # Test Status: Incomplete, In-progress, complete # 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 # # # # Any # # check if value_at_index is yes # # Field: # # Input: # # Output: # # Description: # # Test Status: Incomplete, In-progress, complete # 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 # # Field: # # Input: # # Output: # # Description: # # Test Status: Incomplete, In-progress, complete # 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 is N/A. # # FDR rules: type of requirement/other circumstances may/may not allow N/A in certain fs # # Field: # # Input: # # Output: # # Description: # # Test Status: Incomplete, In-progress, complete # 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 # # Field: # # Input: # # Output: # # Description: # # Test Status: Incomplete, In-progress, complete # 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 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 # # Field: # # Input: # # Output: # # Description: # # Test Status: Incomplete, In-progress, complete # def has_not_required(value): # if value.lower().find("not required") != -1: # return True # else: # return False # # # """ # SANDBOX # """ # if __name__ == '__main__': # # This is your playground # # call function # # print result # # testval = "blah blah TBD \n anatomy not required (TBD percentile, etc.). \nFunction #Parent = P40-030 #Parent = P40-040" # testout = has_not_required(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") # # """ PKr0OΨ1&&rtm/_old/test_rule_functions.py# """ # # Tests for functions in rule_functions. # # """ # # from rtm._old.rule_functions import * # # # def test_starts_with_p(): # pass_list = ['P000', 'PLLL', ] # pass_output = [] # fail_list = ['J000', '0000', ] # fail_output = [] # for i in pass_list: # pass_output.append(starts_with_p(i)) # for j in fail_list: # fail_output.append(starts_with_p(j)) # assert all(pass_output) is True # assert not any(fail_output) is True # # # def test_has_only_digits_after_first(): # pass_list = ['P000', 'P999', '9999', ] # pass_output = [] # fail_list = ['JJJJ', 'P0R0', '', ] # fail_output = [] # for i in pass_list: # pass_output.append(has_only_digits_after_first(i)) # for j in fail_list: # fail_output.append(has_only_digits_after_first(j)) # assert all(pass_output) is True # assert not any(fail_output) is True # # # def test_has_only_three_digits(): # pass_list = ['P000', 'P999', 'J999', ] # pass_output = [] # fail_list = ['P1111', 'P0R0', '', 'P55', ] # fail_output = [] # for i in pass_list: # pass_output.append(has_only_three_digits(i)) # for j in fail_list: # fail_output.append(has_only_three_digits(j)) # assert all(pass_output) is True # assert not any(fail_output) is True # # # def test_has_only_six_digits(): # pass_list = ['P000-666', 'P999000', 'P5-55999', ] # pass_output = [] # fail_list = ['P55-555', 'P5-5555', '', 'P555-55', ] # fail_output = [] # for i in pass_list: # pass_output.append(has_only_six_digits(i)) # for j in fail_list: # fail_output.append(has_only_six_digits(j)) # assert all(pass_output) is True # assert not any(fail_output) is True # # # def test_has_hyphen(): # pass_list = ['P000-666', 'P9990-00', 'P5-559-99', ] # pass_output = [] # fail_list = ['P55555', 'P010', '', ] # fail_output = [] # for i in pass_list: # pass_output.append(has_hyphen(i)) # for j in fail_list: # fail_output.append(has_hyphen(j)) # assert all(pass_output) is True # assert not any(fail_output) is True # # # def test_has_single_hyphen_positioned(): # pass_list = ['P000-666', 'jjjj-jjj', ] # pass_output = [] # fail_list = ['P55-555', 'P5-5555', '', 'P555-555-', "P-111-111"] # fail_output = [] # for i in pass_list: # pass_output.append(has_single_hyphen_positioned(i)) # for j in fail_list: # fail_output.append(has_single_hyphen_positioned(j)) # assert all(pass_output) is True # assert not any(fail_output) is True # # # def test_is_capital_x(): # pass_list = ['X', 'X ', ' X', ' X ', 'X ', ] # pass_output = [] # fail_list = ['x', 'f', '-', ] # fail_output = [] # for i in pass_list: # pass_output.append(is_capital_x(i)) # for j in fail_list: # fail_output.append(is_capital_x(j)) # assert all(pass_output) is True # assert not any(fail_output) is True # # # def test_is_lower_x(): # pass_list = ['x', 'x ', ' x', ' x ', 'x ', ] # pass_output = [] # fail_list = ['X', 'f', '-', ] # fail_output = [] # for i in pass_list: # pass_output.append(is_lower_x(i)) # for j in fail_list: # fail_output.append(is_lower_x(j)) # assert all(pass_output) is True # assert not any(fail_output) is True # # # def test_is_capital_f(): # pass_list = ['F', 'F ', ' F', ' F ', 'F ', ] # pass_output = [] # fail_list = ['X', 'f', '-', ] # fail_output = [] # for i in pass_list: # pass_output.append(is_capital_f(i)) # for j in fail_list: # fail_output.append(is_capital_f(j)) # assert all(pass_output) is True # assert not any(fail_output) is True # # # def test_is_lower_f(): # pass_list = ['f', 'f ', ' f', ' f ', 'f ', ] # pass_output = [] # fail_list = ['X', 'F', '-', '', ' '] # fail_output = [] # for i in pass_list: # pass_output.append(is_lower_f(i)) # for j in fail_list: # fail_output.append(is_lower_f(j)) # assert all(pass_output) is True # assert not any(fail_output) is True # # # def test_is_procedure_step(): # pass_list = ['procedure step', 'PROCEDURE STEP', ' procedure step', 'procedure step ', ' procedure step ', ] # pass_output = [] # fail_list = [' ', 'procedurestep', 'procedure_step', 'procedure stel', 'user need', 'surgical step', ] # fail_output = [] # for i in pass_list: # pass_output.append(is_procedure_step(i)) # for j in fail_list: # fail_output.append(is_procedure_step(j)) # assert all(pass_output) is True # assert not any(fail_output) is True # # # def test_is_user_need(): # pass_list = ['user need', 'USER NEED', ' user need', 'user need ', ' user need ', ] # pass_output = [] # fail_list = [' ', 'userneed', 'user_need', 'users need', 'procedure step', 'need', ] # fail_output = [] # for i in pass_list: # pass_output.append(is_user_need(i)) # for j in fail_list: # fail_output.append(is_user_need(j)) # assert all(pass_output) is True # assert not any(fail_output) is True # # # def test_is_risk_need(): # pass_list = ['risk need', 'RISK NEED', ' risk need', 'risk need ', ' risk need ', ] # pass_output = [] # fail_list = [' ', 'riskneed', 'risk_need', 'risk needs', 'procedure step', 'need', ] # fail_output = [] # for i in pass_list: # pass_output.append(is_risk_need(i)) # for j in fail_list: # fail_output.append(is_risk_need(j)) # assert all(pass_output) is True # assert not any(fail_output) is True # # # def test_is_business_need(): # pass_list = ['business need', 'BUSINESS NEED', ' business need', 'business need ', ' business need ', ] # pass_output = [] # fail_list = [' ', 'businessneed', 'business_need', 'business needs', 'procedure step', 'need', ] # fail_output = [] # for i in pass_list: # pass_output.append(is_business_need(i)) # for j in fail_list: # fail_output.append(is_business_need(j)) # assert all(pass_output) is True # assert not any(fail_output) is True # # # def test_is_design_input(): # pass_list = ['design input', 'DESIGN INPUT', ' design input', 'design input ', ' design input ', ] # pass_output = [] # fail_list = [' ', 'designinput', 'design_input', 'design inputs', 'procedure step', 'input', ] # fail_output = [] # for i in pass_list: # pass_output.append(is_design_input(i)) # for j in fail_list: # fail_output.append(is_design_input(j)) # assert all(pass_output) is True # assert not any(fail_output) is True # # # def test_is_design_output(): # pass_list = ['design output', 'DESIGN OUTPUT', ' design output', 'design output ', ' design output ', ] # pass_output = [] # fail_list = [' ', 'designoutput', 'design_output', 'design outputs', 'procedure step', 'output', ] # fail_output = [] # for i in pass_list: # pass_output.append(is_design_output(i)) # for j in fail_list: # fail_output.append(is_design_output(j)) # assert all(pass_output) is True # assert not any(fail_output) is True # # # def test_is_cascade_lvl_approved(): # pass_list = [ # 'design output', 'DESIGN INPUT', ' user need', 'design input ', ' procedure step ', ] # pass_output = [] # fail_list = [' ', 'designoutput', 'user_need', 'design inputs', 'need', 'output', ] # fail_output = [] # for i in pass_list: # pass_output.append(is_cascade_lvl_approved(i)) # for j in fail_list: # fail_output.append(is_cascade_lvl_approved(j)) # assert all(pass_output) is True # assert not any(fail_output) is True # # # def test_has_hashtag(): # pass_list = ['\n#MatingParts', 'Instrument A needs to thread into Instrument B\n#MatingParts', '#function', ] # pass_output = [] # fail_list = [' ', '#', 'Instrument A has overall length of 15 mm +/- TBD', 'WC #0000567492', '# Function', ] # fail_output = [] # for i in pass_list: # pass_output.append(has_hashtag(i)) # for j in fail_list: # fail_output.append(has_hashtag(j)) # assert all(pass_output) is True # assert not any(fail_output) is True # # # def test_count_hashtags(): # pass_list = ['#Function', 'Instrument A ...with Instrument B\n#MatingParts', '#MatingParts\n#Function\n#Parent', ] # pass_output = [] # fail_list = [' ', '#', 'Instrument A has overall length of 15 mm +/- TBD', 'Windchill #0000768572', '# Function'] # fail_output = [] # for i in pass_list: # pass_output.append(count_hashtags(i)) # for j in fail_list: # fail_output.append(count_hashtags(j)) # assert pass_output == [1, 1, 3] # assert fail_output == [0, 0, 0, 0, 0] # # # def test_has_hashtag_function(): # pass_list = ['Instrument A shall have overall length within +/- TBD\n#Function', '#function'] # pass_output = [] # fail_list = [' ', '--', 'Instrument A has overall length of 15 mm +/- TBD\n#MatingParts', '#matingparts', ] # fail_output = [] # for i in pass_list: # pass_output.append(has_hashtag_function(i)) # for j in fail_list: # fail_output.append(has_hashtag_function(j)) # assert all(pass_output) is True # assert not any(fail_output) is True # # # def test_has_hashtag_mating_parts(): # pass_list = ['Instrument A shall have overall length within +/- TBD\n#MatingParts', '#matingparts'] # pass_output = [] # fail_list = [' ', '--', 'Instrument A has overall length of 15 mm +/- TBD\n#Function', '#function', ] # fail_output = [] # for i in pass_list: # pass_output.append(has_hashtag_mating_parts(i)) # for j in fail_list: # fail_output.append(has_hashtag_mating_parts(j)) # assert all(pass_output) is True # assert not any(fail_output) is TruePKr0OzѪ rtm/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 ------------------------------------------------ # None # --- Third Party Imports ----------------------------------------------------- # None # --- Intra-Package Imports --------------------------------------------------- import rtm.main.context_managers as context from rtm.containers.worksheet_columns import get_matching_worksheet_columns 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 def print(self): """Print to console 1) field name and 2) the field's validation results.""" for result in self._val_results: result.print() def __str__(self): return self.__class__, self.found if __name__ == "__main__": pass PKr0ON-N-rtm/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.main.context_managers as context import rtm.validate.validation as val from rtm.main import context_managers as context from rtm.validate.validator_output import OutputHeader 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""" for field_ in self: field_.print() # --- 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 = [ OutputHeader(self.name), val.val_column_exist(self.found), ] if self.found: self._val_results += [ val.val_column_sort(self), # No need for explicit "not empty" check b/c this is caught by pxxx # format and val_match_parent_prefix. val.val_unique_values(self.values), val.val_alphabetical_sort(self.values), val.val_root_id_format(self.values, work_items), val.val_nonroot_ids_start_w_root_id(), ] @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 = [ OutputHeader(self.name), # Start with header val.val_column_exist(self.found), ] if self.found: self._val_results += [ val.val_column_sort(self), val.val_cascade_block_not_empty(), val.val_cascade_block_only_one_entry(), val.val_cascade_block_x_or_f(), val.val_cascade_block_use_all_columns(), ] # --- 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 = [ OutputHeader(self.name), val.val_column_exist(self.found), ] if self.found: self._val_results += [ val.val_column_sort(self), val.val_cells_not_empty(self.values), val.valid_cascade_levels(self), val.val_matching_cascade_levels(), ] @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") def validate(self): """Validate this field""" self._val_results = [ OutputHeader(self.name), val.val_column_exist(self.found), ] if self.found: self._val_results += [ val.val_column_sort(self), ] @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 = [ OutputHeader(self.name), val.val_column_exist(self.found), ] if self.found: self._val_results += [ val.val_column_sort(self), val.val_cells_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 = [ OutputHeader(self.name), val.val_column_exist(self.found), ] if self.found: self._val_results += [ val.val_column_sort(self), val.val_cells_not_empty(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 = [ OutputHeader(self.name), # Start with header val.val_column_exist(self.found), ] if self.found: self._val_results += [ val.val_column_sort(self), ] @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 = [ OutputHeader(self.name), # Start with header val.val_column_exist(self.found), ] if self.found: self._val_results += [ val.val_column_sort(self), val.val_cells_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 = [ OutputHeader(self.name), # Start with header val.val_column_exist(self.found), ] if self.found: self._val_results += [ val.val_column_sort(self), val.val_cells_not_empty(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 = [ OutputHeader(self.name), # Start with header val.val_column_exist(self.found), ] if self.found: self._val_results += [ val.val_column_sort(self), val.val_cells_not_empty(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 PKr0O 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 @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 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 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)] # --- 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) PKU+ON>˱ #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 --------------------------------------------------- from rtm.main.exceptions import RTMValidatorFileError import rtm.main.context_managers as context 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_name): # --- Attributes ------------------------------------------------------ self.max_row = None self.cols = None self.height = 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.height = self.max_row - 1 # --- Convert Worksheet to WorksheetColumn objects ---------------- columns = [] start_column_num = 1 for position, col in enumerate(range(start_column_num, ws.max_column + 1)): column_header = ws.cell(1, col).value column_values = tuple(ws.cell(row, col).value for row in range(2, self.max_row + 1)) ws_column = WorksheetColumn( header=column_header, values=column_values, position=position, column=col ) columns.append(ws_column) self.cols = columns 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 PKr0O*Vrtm/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 --------------------------------------------------- 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(path=None): """This is the main function.""" 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: if not path: path = get_rtm_path() with context.path.set(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() PKU+OUrpprtm/main/cli.py"""This is pretty boiler plate right now. Later, as more command line options are added in, this module will become more substantial.""" # --- Standard Library Imports ------------------------------------------------ # None # --- Third Party Imports ----------------------------------------------------- import click # --- Intra-Package Imports --------------------------------------------------- from rtm.main import api @click.command() def main(): """`rtm` on the command line will run the this function. Later, this will have more functionality. That's why it appear superfluous right now""" api.main() PKU+O~hhrtm/main/context_managers.py"""This is how the major building blocks of the app communicate with each other. This may be overkill and I may simplify it later, but it is what it is for now.""" # --- 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""" # TODO This system can and probably should be superseded by something simpler. self._name = name self._value = None @contextmanager def set(self, value): """Set this object equal to something to make it available to calling functions.""" self._value = value yield self._value = None def get(self): """This is how functions access the values set above.""" 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') PKU+Ozzrtm/main/excel.py"""This module focuses on getting and validating the path for the RTM worksheet.""" # --- Standard Library Imports ------------------------------------------------ import tkinter as tk from pathlib import Path from tkinter import filedialog # --- Third Party Imports ----------------------------------------------------- import click # --- Intra-Package Imports --------------------------------------------------- from rtm.main import exceptions as exc def get_rtm_path(path_option='default') -> 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 pathPKU+Ohs@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 PKr0O41--rtm/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 ------------------------------------------------ # None # --- Third Party Imports ----------------------------------------------------- # None # --- Intra-Package Imports --------------------------------------------------- # None 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)) } if __name__ == "__main__": pass PKr0O/׋%.%.rtm/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 # --- 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 # --- General-purpose validation ---------------------------------------------- def val_column_exist(field_found) -> ValidationResult: """Given field_found=True/False, return a ValidationResult, ready to be printed to console.""" title = "Field Exist" if field_found: score = "Pass" explanation = None else: score = "Error" explanation = "Field not found. Make sure your headers exactly match the title shown above." return ValidationResult(score, title, explanation) def val_column_sort(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) def val_cells_not_empty(values) -> ValidationResult: """All cells must be non-empty""" title = "Not 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, explanation, error_indices) # --- ID ---------------------------------------------------------------------- def val_root_id_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.""" title = 'Procedure Step Format' 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) ] 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) def val_unique_values(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) def val_nonroot_ids_start_w_root_id(): """Each work item starts with its root ID""" title = "Start with 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) 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) def val_alphabetical_sort(values): """Each cell in the ID field should come alphabetically after the prior one.""" title = 'Alphabetical Sort' 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) 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) # --- Cascade Block ----------------------------------------------------------- def val_cascade_block_not_empty(): """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) def val_cascade_block_only_one_entry(): """Each row in cascade block must contain only one entry""" title = "Single Entry" # indices = [] # for work_item in context.work_items.get(): # _len = len(work_item.cascade_block) # if _len != 1: # indices.append(work_item.index) 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 = "Error" explanation = ( "Action Required. The following rows are blank or have multiple entries: " ) return ValidationResult(score, title, explanation, error_indices) def val_cascade_block_x_or_f() -> ValidationResult: """Value in first position must be X or F.""" title = "X or F" 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, ) ] 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) def val_cascade_block_use_all_columns(): """The cascade block shouldn't have any unused columns.""" title = "Use All 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 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) # --- Cascade Level ----------------------------------------------------------- def valid_cascade_levels(field): """Check cascade levels against list of acceptable entries.""" title = 'Valid Entries' values = field.values error_indices = [ index for index, value in enumerate(values) if not checks.cell_empty(value) and value not in checks.allowed_cascade_levels.keys() ] 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(checks.allowed_cascade_levels.keys())}:\n\t\t\t' return ValidationResult(score, title, explanation, error_indices) def val_matching_cascade_levels(): """A work item's cascade level entry must match its cascade block entry.""" fields = context.fields.get() cascade_level = fields.get_field_object('CascadeLevel') title = 'Matching Levels' body = cascade_level.values # --- Don't report on rows that failed for other reasons (i.e. blank or invalid input exclude_results = [ val_cells_not_empty(body), valid_cascade_levels(cascade_level), val_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) 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) if __name__ == "__main__": pass PKU+O m m 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 from itertools import groupby, count # --- Third Party Imports ----------------------------------------------------- import click # --- Intra-Package Imports --------------------------------------------------- # None 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))) 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, explanation=None, nonconforming_indices=None): self._scores_and_colors = {'Pass': 'green', 'Warning': 'yellow', 'Error': 'red'} self.score = score self._title = title self._explanation = explanation self.indices = nonconforming_indices @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_row = 2 # this is the row # directly after the headers return [index + first_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 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.16.dist-info/entry_points.txtN+I/N.,()**ɵb0[GR9ɤ욭&$Ø {=f,eƹ Ŝˌk]g/:sLZze %5%u&fcUGP)RmD> rQþwTJTo'`A\𩰒az%$Fʹ":L_:ؒ@N)7=B$0`G @J*cs@cr43h]5y^wz&,S+Jһsv0f]ww^1?:&A@{Óqހe,`SȼI;̞K}0sX KR,QIP\;BqjKgF5F@DR~Ezc}菮'lpu3wgHh2n{Ąe7y&"n(pP,Gv͙;3euv}i-6ΆS(i4_?ɑ';DcOnyJ:xԓQ>t%zٽնNc]ufl6)G9-TO4YЍVT#1BGn D,{ M6!Y wӦOPsM:8Óqs$[w1Zg\& kuk]ﺁA 8.|zqXA3PB9ve=Loݞ2ުnm+(jC}"#o{Ed"th[ďh(M~cJŒ`$Vtӡ',Xxt?!8zwۓ6C;|K'6[-d\$/hoInDR8r1vlmQZ"EY+|u\7A %60=BWyQ n|C?h^.eWYN$x%d)Rc]z꺽߭vDAH/AFqjdMs%$r(|ysF].LV9x 1rэAB lL,5`.+)"(c)oN"u]*%^bێ[qj^m͚|tD?$3DF?ubΑhAnLoVKrُU#5tqƹfI僌VS_k&1ar#jsB&|Agv: AhݎKnLwu3.35P@%CG1r EK E- JN 1gzݝ@7MxMC^<1#Ogofg" {nŖ7/ s1~bZ."gqWLgX),bOΘj22^Ծxv I-a5s9<=b`@+| ov'Q7uY%~cV{ CiI=0E04'Y5 V<)9d+/k5ݕ|{" *F7ttDP)ⱉEaE_WElYL骷TNZ<DRܥ€O1@_ '$::FkX`$ŹdV b[{ g?mtLFz% `j qW6@Ul g7[Z -s;?-2;򻃄UF@8*숊搸A|o U=AEG\ֶ^oH7Hdq]W{pe׺dߏx^T<#]#pGb˱ #rtm/containers/worksheet_columns.pyPKr0O*Vrtm/main/api.pyPKU+OUrpprtm/main/cli.pyPKU+O~hhrtm/main/context_managers.pyPKU+Ozz.rtm/main/excel.pyPKU+Ohs@qrtm/main/exceptions.pyPKr0O41--rtm/validate/checks.pyPKr0O/׋%.%. rtm/validate/validation.pyPKU+O m m x:rtm/validate/validator_output.pyPK!H)YD'))#Hdps_rtm-0.1.16.dist-info/entry_points.txtPKU+O8<< Hdps_rtm-0.1.16.dist-info/LICENSEPK!HPO Mdps_rtm-0.1.16.dist-info/WHEELPK!HͿ;X !Mdps_rtm-0.1.16.dist-info/METADATAPK!H( GWdps_rtm-0.1.16.dist-info/RECORDPK[