# 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 += ("Analyzed Requirement for Change | "
"Impacted Requirements | "
"Impacted Systems/Subsystems | "
"Impacted Test Procedures | ")
# 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 += "
"
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()