# Valispace Script for Custom Actions # Purpose: Creates an Impact Analysis Report showing which Requirements, Components and Test Runs might be impacted by a change in the Req # Developed by [Paul Grey] # Date: [Date] #IMPORTANT: Provide your valispace url in the Configuration Section below from typing import Any, Dict from valispace import API from datetime import datetime from time import perf_counter # --------------------------------------------- # Configuration Section VALISPACE = { "domain": "https://YOUR_DEPLOYMENT_NAME.valispace.com", # Base URL for the Valispace instance "warn_https": False, # Disable HTTPS warnings if set to False } # --------------------------------------------- def initialize_api(temporary_access_token) -> API: """ Initialize the Valispace API. Returns: API: Initialized Valispace API object. """ return API( url=VALISPACE["domain"], #session_token=temporary_access_token, warn_https=VALISPACE.get("warn_https", False), username='paul', password='paul' ) def fetch_ancestors(api, req_id, visited=None): if visited is None: visited = set() if req_id in visited: return set() visited.add(req_id) ancestors = set() try: # Fetch the requirement details requirement = api.request('GET', f'requirements/{req_id}/') # Immediate parents parents = set(requirement["parents"]) except Exception as e: print(f"Error fetching requirement: {e}") parents = set() ancestors.update(parents) # Recursively fetch for each parent for parent_id in parents: ancestors.update(fetch_ancestors(api, parent_id, visited)) return ancestors def fetch_descendants(api, req_id, visited=None): if visited is None: visited = set() if req_id in visited: return set() visited.add(req_id) descendants = set() try: # Fetch the requirement details requirement = api.request('GET', f'requirements/{req_id}/') # Immediate children children = set(requirement["children"]) except Exception as e: print(f"Error fetching requirement: {e}") children = set() descendants.update(children) # Recursively fetch for each child for child_id in children: descendants.update(fetch_descendants(api, child_id, visited)) return descendants def fetch_requirements_recursive(api, req_id): ancestors = fetch_ancestors(api, req_id) descendants = fetch_descendants(api, req_id) # Combine ancestors and descendants, including the original requirement all_related = ancestors.union(descendants).union({req_id}) ancestors_decendants = ancestors.union(descendants) return all_related, ancestors_decendants def gather_verification_methods_and_components(req_ids, api): verifications = set() components = set() test_procedure_names = set() for req_id in req_ids: try: requirement = api.request('GET', f'requirements/{req_id}/') verifications.update(requirement["verification_methods"]) except: continue for ver in verifications: req_vm = api.request('GET', f'requirements/requirement-vms/{ver}/') try: cvms = req_vm["component_vms"] for cvm in cvms: component_vm = api.request('GET', f'requirements/component-vms/{cvm}/') component_id = component_vm["component"] component = api.request('GET', f'components/{component_id}/') component_name = component['name'] if component_name not in components: components.update([component_name]) except: print(f"No Component on Verification Method {req_vm['id']}") try: if req_vm["test_procedure_steps"]: for test_procedure_step_id in req_vm["test_procedure_steps"]: test_procedure_step = api.request('GET', f'testing/test-procedure-steps/{test_procedure_step_id}/') test_procedure_id = test_procedure_step["test_procedure"] test_procedure = api.request('GET', f'testing/test-procedures/{test_procedure_id}/') test_procedure_names.update([test_procedure["name"]]) except: print(f"No Test Procedure associated with {req_vm['id']}") return components, test_procedure_names def req_id_and_text_to_link (req_id, req_identifier): return f"${req_identifier}" def convert_json_to_html_table(req, impact_analysis, api) -> str: """ Convert JSON data from impacted_req into an HTML table. Args: impacted_req (dict): JSON data with inconsistency results. Returns: str: HTML table string. """ html_table = "" # Add table headers html_table += ("" "" "" "") # Process each impacted_req and add to the table # Process each requirement ID identifier, text = fetch_requirement_details(req, api) req_detail = (req_id_and_text_to_link(req, identifier) + f" - {text}") # Get Identifier and Text of Impacted Reqs impacted_req_details = [] for impacted_req in impact_analysis[str(req)]['relations']: # Process each requirement ID identifier, text = fetch_requirement_details(impacted_req, api) if identifier: impacted_req_details.append(req_id_and_text_to_link(impacted_req, identifier)) else: continue # Join multiple requirements with a newline impacted_req_details_str = ", ".join(impacted_req_details) tests = ", ".join(impact_analysis[str(req)]["test_procedures"]) components = ", ".join(impact_analysis[str(req)]["components"]) html_table += ("" "" "" "".format( req_detail, impacted_req_details_str, components, tests)) html_table += "

Analyzed Requirement for Change

Impacted Requirements

Impacted Systems/Subsystems

Impacted Test Procedures

{}

{}

{}

{}

" return html_table def fetch_requirement_details(req_id, api): try: req = api.request('GET', f'requirements/{req_id}/') identifier = req['identifier'] text = req['text'] return identifier, text except: identifier = False text = False return identifier, text def create_analysis_report(api, project_id, impact_analysis, specs, custom_action_objects_ids, folder_id) -> Dict[str, Any]: """ Create a new Analysis in the Analysis Module and add a text block with the report text. Args: api (API): Initialized Valispace API object. project_id (int): Project ID. report_text (str): Text to be included in the report. Returns: Dict[str, Any]: Response from the API after creating the report. """ #Get current Date and Time current_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # Create a new Analysis analysis = api.request('POST', 'analyses/', { "project": project_id, "folder": folder_id, "name": "ValiAssistant Change-Impact Report " + current_datetime }) # Fetch Specification Names spec_names = [fetch_specification_name(spec_id, api) for spec_id in specs] report_title = "Change-Impacts for: " + ", ".join(spec_names) # Create a Text Block for the Report Title title_column = api.request('POST', 'analyses/columns/', { "block_type": 10, "block_data": {"style": "h1", "target_analysis_id": analysis["id"]}, "position": 5, # Adjust position as needed "new_row": {"position": 20, "analysis": analysis["id"]} # Adjust position as needed }) # Fetch the Title Block ID title_block_id = api.request('GET', f'analyses/blocks/{title_column["id"]}/')['content_object']['id'] # Update the Title Text Field api.request('PATCH', f'analyses/blocks/text/{title_block_id}/', { "text": report_title }) shift = 30 for custom_action_objects_id in custom_action_objects_ids: # Create a Text Block for the Report Title title_column = api.request('POST', 'analyses/columns/', { "block_type": 10, "block_data": {"style": "h2", "target_analysis_id": analysis["id"]}, "position": 10, # Adjust position as needed "new_row": {"position": shift, "analysis": analysis["id"]} # Adjust position as needed }) shift += 10 # Fetch the Title Block ID title_block_id = api.request('GET', f'analyses/blocks/{title_column["id"]}/')['content_object']['id'] # Update the Title Text Field identifier, text = fetch_requirement_details(custom_action_objects_id, api) api.request('PATCH', f'analyses/blocks/text/{title_block_id}/', { "text": 'Change-Impact Table for Requirement ' + identifier }) # ------------------------------------------------------------------------------------------------------- # Create a new Text Block for Change-Impact Table column = api.request('POST', 'analyses/columns/', { "block_type": 10, "block_data": {"style": "p", "target_analysis_id": analysis["id"]}, "position": 10, "new_row": {"position": shift, "analysis": analysis["id"]} }) shift += 10 # Fetch the Block ID block_id = api.request('GET', f'analyses/blocks/{column["id"]}/')['content_object']['id'] # Update the Text Field html_table = convert_json_to_html_table(custom_action_objects_id, impact_analysis, api) api.request('PATCH', f'analyses/blocks/text/{block_id}/', { "text": html_table }) return 0 """ # ------------------------------------------------------------------------------------------------------- # Create a Text Block for the Report Title title_column = api.request('POST', 'analyses/columns/', { "block_type": 10, "block_data": {"style": "h3", "target_analysis_id": analysis["id"]}, "position": 10, # Adjust position as needed "new_row": {"position": shift, "analysis": analysis["id"]} # Adjust position as needed }) shift += 10 # Fetch the Title Block ID title_block_id = api.request('GET', f'analyses/blocks/{title_column["id"]}/')['content_object']['id'] # Update the Title Text Field identifier, text = fetch_requirement_details(custom_action_objects_id, api) api.request('PATCH', f'analyses/blocks/text/{title_block_id}/', { "text": 'ValiAssistant AI Analysis' }) # ------------------------------------------------------------------------------------------------------- # Text Block for Paragraph of AI Assessment column = api.request('POST', 'analyses/columns/', { "block_type": 10, "block_data": {"style": "p", "target_analysis_id": analysis["id"]}, "position": 10, "new_row": {"position": shift, "analysis": analysis["id"]} }) shift += 10 # Fetch the Block ID block_id = api.request('GET', f'analyses/blocks/{column["id"]}/')['content_object']['id'] # Update the Text Field ai_assessment = generate_ai_assessment(custom_action_objects_id, impact_analysis, api) ai_assessment = ai_assessment['results'] api.request('PATCH', f'analyses/blocks/text/{block_id}/', { "text": ai_assessment }) # ------------------------------------------------------------------------------------------------------- return 0 """ def fetch_specification_name(spec_id, api): """ Fetch the name of a specification given its ID. Args: spec_id (int): Specification ID. api (API): Initialized Valispace API object. Returns: str: Name of the specification. """ spec = api.request('GET', f'requirements/specifications/{spec_id}/') return spec['name'] def generate_analysis_procedure_folder(api: API, project_id): folder_name = 'ValiAssistant Reports' folders = api.request('GET', f'documents/folders/?project={project_id}') for folder in folders: if folder['name'] == folder_name: return folder['id'] else: continue response = api.request('POST', 'documents/folders/', {"project": project_id, "name": "ValiAssistant Reports"}) return response['id'] def generate_ai_assessment(custom_action_objects_id, impact_analysis, api): impacted_reqs = impact_analysis[str(custom_action_objects_id)]['relations'] current_req_text = api.request('GET', f'requirements/{custom_action_objects_id}/?&clean_text=text')['text'] custom_prompt1 = f""" Consider this main Requirement Text for a Product: {current_req_text} I would like to make a Change-Impact assessment. Change-Impact assessment provides information on how a change on this main requirement will impact a set of other Requirements. So, based on the following list of Requirements please provide me information on which Requirements will be impacted the most, and why, by a change in the main Requirement above. This paragraph will be read by an Engineer so make it as technical as possible. """ custom_prompt2 = """ Please provide impact information strictly in the following format: In the "results" DO NOT return a string but strictly list of JSON dictionaries in the form of [{'object_id1': 'Impact Assessment Description1'},{'object_id2': 'Impact Assessment Description2'},]. DO NOT include the 'object_id' in the 'Impact Assessment Description'! Do this for each requirement in the list that you consider has a moderate or high impact. Specify whether the impact is "High" or "Moderate". For the description be as technical as possible. The list of requirements are: """ custom_prompt = custom_prompt1+custom_prompt2 ai_assessment = api.general_prompt( custom_prompt=custom_prompt, model="requirement", field="text", objects_ids=impacted_reqs, parallel=False, replace_valis_by_id=False ) print(ai_assessment) #ai_assessments = ai_assessments['results'] #for ai_assessment in ai_assessments: #ai_assessment_paragraph return ai_assessment def main(**kwargs) -> Dict[str, Any]: t1 = perf_counter() """ Main function to execute the script. Args: **kwargs: Additional arguments passed to the script. Returns: Dict[str, Any]: Result data to be sent back to Valispace. """ api = initialize_api(kwargs['temporary_access_token']) custom_action_objects_ids = kwargs.get("objects_ids", []) #custom_action_objects_ids = [4033] project_id = api.request('GET', f'requirements/{custom_action_objects_ids[0]}')['project'] impact_analysis = {} spec_ids = set() for req_id in custom_action_objects_ids: parent_children_list, ancestors_decendants = fetch_requirements_recursive(api, req_id) components, test_procedures = gather_verification_methods_and_components(parent_children_list, api) impact_analysis[str(req_id)] = { "relations": list(ancestors_decendants), "components": list(components), "test_procedures": list(test_procedures) } req = api.request('GET', f'requirements/{req_id}/') spec_ids.add(req['specification']) folder_id = generate_analysis_procedure_folder(api, project_id) create_analysis_report(api, project_id, impact_analysis, list(spec_ids), custom_action_objects_ids, folder_id) t2 = perf_counter() print(t2-t1, " s") if __name__=='__main__': main()