spdx_tools.spdx.parser.jsonlikedict.relationship_parser
1# SPDX-FileCopyrightText: 2022 spdx contributors 2# 3# SPDX-License-Identifier: Apache-2.0 4from beartype.typing import Dict, List, Optional 5 6from spdx_tools.common.typing.constructor_type_errors import ConstructorTypeErrors 7from spdx_tools.spdx.model import Relationship, RelationshipType 8from spdx_tools.spdx.parser.error import SPDXParsingError 9from spdx_tools.spdx.parser.jsonlikedict.dict_parsing_functions import ( 10 delete_duplicates_from_list, 11 json_str_to_enum_name, 12 parse_field_or_log_error, 13 parse_field_or_no_assertion_or_none, 14) 15from spdx_tools.spdx.parser.logger import Logger 16from spdx_tools.spdx.parser.parsing_functions import ( 17 construct_or_raise_parsing_error, 18 raise_parsing_error_if_logger_has_messages, 19) 20 21 22class RelationshipParser: 23 logger: Logger 24 25 def __init__(self): 26 self.logger = Logger() 27 28 def parse_all_relationships(self, input_doc_dict: Dict) -> List[Relationship]: 29 relationships = [] 30 relationship_dicts: List[Dict] = input_doc_dict.get("relationships", []) 31 relationships.extend( 32 parse_field_or_log_error(self.logger, relationship_dicts, self.parse_relationship, [], True) 33 ) 34 35 document_describes: List[str] = delete_duplicates_from_list(input_doc_dict.get("documentDescribes", [])) 36 doc_spdx_id: Optional[str] = input_doc_dict.get("SPDXID") 37 38 existing_relationships_without_comments: List[Relationship] = self.get_all_relationships_without_comments( 39 relationships 40 ) 41 relationships.extend( 42 parse_field_or_log_error( 43 self.logger, 44 document_describes, 45 lambda x: self.parse_document_describes( 46 doc_spdx_id=doc_spdx_id, 47 described_spdx_ids=x, 48 existing_relationships=existing_relationships_without_comments, 49 ), 50 [], 51 ) 52 ) 53 54 package_dicts: List[Dict] = input_doc_dict.get("packages", []) 55 existing_relationships_without_comments: List[Relationship] = self.get_all_relationships_without_comments( 56 relationships 57 ) 58 59 relationships.extend( 60 parse_field_or_log_error( 61 self.logger, 62 package_dicts, 63 lambda x: self.parse_has_files( 64 package_dicts=x, existing_relationships=existing_relationships_without_comments 65 ), 66 [], 67 ) 68 ) 69 70 file_dicts: List[Dict] = input_doc_dict.get("files", []) 71 72 # not implemented yet: deal with deprecated fields in file: 73 # https://github.com/spdx/tools-python/issues/294 & https://github.com/spdx/tools-python/issues/387 74 _ = self.parse_artifact_of(file_dicts=file_dicts) 75 _ = self.parse_file_dependencies(file_dicts=file_dicts) 76 77 raise_parsing_error_if_logger_has_messages(self.logger) 78 79 return relationships 80 81 def parse_relationship(self, relationship_dict: Dict) -> Relationship: 82 logger = Logger() 83 spdx_element_id: Optional[str] = relationship_dict.get("spdxElementId") 84 related_spdx_element: Optional[str] = parse_field_or_no_assertion_or_none( 85 relationship_dict.get("relatedSpdxElement") 86 ) 87 relationship_type: Optional[RelationshipType] = parse_field_or_log_error( 88 logger, relationship_dict.get("relationshipType"), self.parse_relationship_type 89 ) 90 relationship_comment: Optional[str] = relationship_dict.get("comment") 91 raise_parsing_error_if_logger_has_messages(logger, "Relationship") 92 93 relationship = construct_or_raise_parsing_error( 94 Relationship, 95 dict( 96 spdx_element_id=spdx_element_id, 97 relationship_type=relationship_type, 98 related_spdx_element_id=related_spdx_element, 99 comment=relationship_comment, 100 ), 101 ) 102 return relationship 103 104 @staticmethod 105 def parse_relationship_type(relationship_type_str: str) -> RelationshipType: 106 try: 107 relationship_type = RelationshipType[json_str_to_enum_name(relationship_type_str)] 108 except KeyError: 109 raise SPDXParsingError([f"Invalid RelationshipType: {relationship_type_str}"]) 110 return relationship_type 111 112 def parse_document_describes( 113 self, doc_spdx_id: str, described_spdx_ids: List[str], existing_relationships: List[Relationship] 114 ) -> List[Relationship]: 115 logger = Logger() 116 describes_relationships = [] 117 for spdx_id in described_spdx_ids: 118 try: 119 describes_relationship = Relationship( 120 spdx_element_id=doc_spdx_id, 121 relationship_type=RelationshipType.DESCRIBES, 122 related_spdx_element_id=spdx_id, 123 ) 124 except ConstructorTypeErrors as err: 125 logger.append(err.get_messages()) 126 continue 127 if not self.check_if_relationship_exists(describes_relationship, existing_relationships): 128 describes_relationships.append(describes_relationship) 129 raise_parsing_error_if_logger_has_messages(logger, "document describes relationships") 130 131 return describes_relationships 132 133 def parse_has_files( 134 self, package_dicts: List[Dict], existing_relationships: List[Relationship] 135 ) -> List[Relationship]: 136 # assume existing relationships are stripped of comments 137 logger = Logger() 138 contains_relationships = [] 139 for package in package_dicts: 140 package_spdx_id: Optional[str] = package.get("SPDXID") 141 contained_files: List[str] = delete_duplicates_from_list(package.get("hasFiles", [])) 142 if not contained_files: 143 continue 144 for file_spdx_id in contained_files: 145 try: 146 contains_relationship = Relationship( 147 spdx_element_id=package_spdx_id, 148 relationship_type=RelationshipType.CONTAINS, 149 related_spdx_element_id=file_spdx_id, 150 ) 151 except ConstructorTypeErrors as err: 152 logger.append(err.get_messages()) 153 continue 154 if not self.check_if_relationship_exists( 155 relationship=contains_relationship, existing_relationships=existing_relationships 156 ): 157 contains_relationships.append(contains_relationship) 158 raise_parsing_error_if_logger_has_messages(logger, "package contains relationships") 159 160 return contains_relationships 161 162 def check_if_relationship_exists( 163 self, relationship: Relationship, existing_relationships: List[Relationship] 164 ) -> bool: 165 # assume existing relationships are stripped of comments 166 if relationship in existing_relationships: 167 return True 168 relationship_inverted: Relationship = self.invert_relationship(relationship) 169 if relationship_inverted in existing_relationships: 170 return True 171 172 return False 173 174 @staticmethod 175 def get_all_relationships_without_comments(existing_relationships: List[Relationship]) -> List[Relationship]: 176 relationships_without_comments = [ 177 Relationship( 178 relationship_type=relationship.relationship_type, 179 related_spdx_element_id=relationship.related_spdx_element_id, 180 spdx_element_id=relationship.spdx_element_id, 181 ) 182 for relationship in existing_relationships 183 ] 184 return relationships_without_comments 185 186 def invert_relationship(self, relationship: Relationship) -> Relationship: 187 return Relationship( 188 related_spdx_element_id=relationship.spdx_element_id, 189 spdx_element_id=relationship.related_spdx_element_id, 190 relationship_type=self.invert_relationship_types[relationship.relationship_type], 191 comment=relationship.comment, 192 ) 193 194 invert_relationship_types = { 195 RelationshipType.DESCRIBES: RelationshipType.DESCRIBED_BY, 196 RelationshipType.DESCRIBED_BY: RelationshipType.DESCRIBES, 197 RelationshipType.CONTAINS: RelationshipType.CONTAINED_BY, 198 RelationshipType.CONTAINED_BY: RelationshipType.CONTAINS, 199 } 200 201 @staticmethod 202 def parse_file_dependencies(file_dicts: List[Dict]) -> List[Relationship]: 203 dependency_relationships = [] 204 # the field fileDependencies is deprecated and should be converted to a relationship 205 # https://github.com/spdx/tools-python/issues/387 206 return dependency_relationships 207 208 @staticmethod 209 def parse_artifact_of(file_dicts: List[Dict]) -> List[Relationship]: 210 generated_relationships = [] 211 # artifactOfs is deprecated and should be converted to an external package and a generated from relationship 212 # https://github.com/spdx/tools-python/issues/294 213 return generated_relationships
class
RelationshipParser:
23class RelationshipParser: 24 logger: Logger 25 26 def __init__(self): 27 self.logger = Logger() 28 29 def parse_all_relationships(self, input_doc_dict: Dict) -> List[Relationship]: 30 relationships = [] 31 relationship_dicts: List[Dict] = input_doc_dict.get("relationships", []) 32 relationships.extend( 33 parse_field_or_log_error(self.logger, relationship_dicts, self.parse_relationship, [], True) 34 ) 35 36 document_describes: List[str] = delete_duplicates_from_list(input_doc_dict.get("documentDescribes", [])) 37 doc_spdx_id: Optional[str] = input_doc_dict.get("SPDXID") 38 39 existing_relationships_without_comments: List[Relationship] = self.get_all_relationships_without_comments( 40 relationships 41 ) 42 relationships.extend( 43 parse_field_or_log_error( 44 self.logger, 45 document_describes, 46 lambda x: self.parse_document_describes( 47 doc_spdx_id=doc_spdx_id, 48 described_spdx_ids=x, 49 existing_relationships=existing_relationships_without_comments, 50 ), 51 [], 52 ) 53 ) 54 55 package_dicts: List[Dict] = input_doc_dict.get("packages", []) 56 existing_relationships_without_comments: List[Relationship] = self.get_all_relationships_without_comments( 57 relationships 58 ) 59 60 relationships.extend( 61 parse_field_or_log_error( 62 self.logger, 63 package_dicts, 64 lambda x: self.parse_has_files( 65 package_dicts=x, existing_relationships=existing_relationships_without_comments 66 ), 67 [], 68 ) 69 ) 70 71 file_dicts: List[Dict] = input_doc_dict.get("files", []) 72 73 # not implemented yet: deal with deprecated fields in file: 74 # https://github.com/spdx/tools-python/issues/294 & https://github.com/spdx/tools-python/issues/387 75 _ = self.parse_artifact_of(file_dicts=file_dicts) 76 _ = self.parse_file_dependencies(file_dicts=file_dicts) 77 78 raise_parsing_error_if_logger_has_messages(self.logger) 79 80 return relationships 81 82 def parse_relationship(self, relationship_dict: Dict) -> Relationship: 83 logger = Logger() 84 spdx_element_id: Optional[str] = relationship_dict.get("spdxElementId") 85 related_spdx_element: Optional[str] = parse_field_or_no_assertion_or_none( 86 relationship_dict.get("relatedSpdxElement") 87 ) 88 relationship_type: Optional[RelationshipType] = parse_field_or_log_error( 89 logger, relationship_dict.get("relationshipType"), self.parse_relationship_type 90 ) 91 relationship_comment: Optional[str] = relationship_dict.get("comment") 92 raise_parsing_error_if_logger_has_messages(logger, "Relationship") 93 94 relationship = construct_or_raise_parsing_error( 95 Relationship, 96 dict( 97 spdx_element_id=spdx_element_id, 98 relationship_type=relationship_type, 99 related_spdx_element_id=related_spdx_element, 100 comment=relationship_comment, 101 ), 102 ) 103 return relationship 104 105 @staticmethod 106 def parse_relationship_type(relationship_type_str: str) -> RelationshipType: 107 try: 108 relationship_type = RelationshipType[json_str_to_enum_name(relationship_type_str)] 109 except KeyError: 110 raise SPDXParsingError([f"Invalid RelationshipType: {relationship_type_str}"]) 111 return relationship_type 112 113 def parse_document_describes( 114 self, doc_spdx_id: str, described_spdx_ids: List[str], existing_relationships: List[Relationship] 115 ) -> List[Relationship]: 116 logger = Logger() 117 describes_relationships = [] 118 for spdx_id in described_spdx_ids: 119 try: 120 describes_relationship = Relationship( 121 spdx_element_id=doc_spdx_id, 122 relationship_type=RelationshipType.DESCRIBES, 123 related_spdx_element_id=spdx_id, 124 ) 125 except ConstructorTypeErrors as err: 126 logger.append(err.get_messages()) 127 continue 128 if not self.check_if_relationship_exists(describes_relationship, existing_relationships): 129 describes_relationships.append(describes_relationship) 130 raise_parsing_error_if_logger_has_messages(logger, "document describes relationships") 131 132 return describes_relationships 133 134 def parse_has_files( 135 self, package_dicts: List[Dict], existing_relationships: List[Relationship] 136 ) -> List[Relationship]: 137 # assume existing relationships are stripped of comments 138 logger = Logger() 139 contains_relationships = [] 140 for package in package_dicts: 141 package_spdx_id: Optional[str] = package.get("SPDXID") 142 contained_files: List[str] = delete_duplicates_from_list(package.get("hasFiles", [])) 143 if not contained_files: 144 continue 145 for file_spdx_id in contained_files: 146 try: 147 contains_relationship = Relationship( 148 spdx_element_id=package_spdx_id, 149 relationship_type=RelationshipType.CONTAINS, 150 related_spdx_element_id=file_spdx_id, 151 ) 152 except ConstructorTypeErrors as err: 153 logger.append(err.get_messages()) 154 continue 155 if not self.check_if_relationship_exists( 156 relationship=contains_relationship, existing_relationships=existing_relationships 157 ): 158 contains_relationships.append(contains_relationship) 159 raise_parsing_error_if_logger_has_messages(logger, "package contains relationships") 160 161 return contains_relationships 162 163 def check_if_relationship_exists( 164 self, relationship: Relationship, existing_relationships: List[Relationship] 165 ) -> bool: 166 # assume existing relationships are stripped of comments 167 if relationship in existing_relationships: 168 return True 169 relationship_inverted: Relationship = self.invert_relationship(relationship) 170 if relationship_inverted in existing_relationships: 171 return True 172 173 return False 174 175 @staticmethod 176 def get_all_relationships_without_comments(existing_relationships: List[Relationship]) -> List[Relationship]: 177 relationships_without_comments = [ 178 Relationship( 179 relationship_type=relationship.relationship_type, 180 related_spdx_element_id=relationship.related_spdx_element_id, 181 spdx_element_id=relationship.spdx_element_id, 182 ) 183 for relationship in existing_relationships 184 ] 185 return relationships_without_comments 186 187 def invert_relationship(self, relationship: Relationship) -> Relationship: 188 return Relationship( 189 related_spdx_element_id=relationship.spdx_element_id, 190 spdx_element_id=relationship.related_spdx_element_id, 191 relationship_type=self.invert_relationship_types[relationship.relationship_type], 192 comment=relationship.comment, 193 ) 194 195 invert_relationship_types = { 196 RelationshipType.DESCRIBES: RelationshipType.DESCRIBED_BY, 197 RelationshipType.DESCRIBED_BY: RelationshipType.DESCRIBES, 198 RelationshipType.CONTAINS: RelationshipType.CONTAINED_BY, 199 RelationshipType.CONTAINED_BY: RelationshipType.CONTAINS, 200 } 201 202 @staticmethod 203 def parse_file_dependencies(file_dicts: List[Dict]) -> List[Relationship]: 204 dependency_relationships = [] 205 # the field fileDependencies is deprecated and should be converted to a relationship 206 # https://github.com/spdx/tools-python/issues/387 207 return dependency_relationships 208 209 @staticmethod 210 def parse_artifact_of(file_dicts: List[Dict]) -> List[Relationship]: 211 generated_relationships = [] 212 # artifactOfs is deprecated and should be converted to an external package and a generated from relationship 213 # https://github.com/spdx/tools-python/issues/294 214 return generated_relationships
def
parse_all_relationships( self, input_doc_dict: dict) -> list[spdx_tools.spdx.model.relationship.Relationship]:
29 def parse_all_relationships(self, input_doc_dict: Dict) -> List[Relationship]: 30 relationships = [] 31 relationship_dicts: List[Dict] = input_doc_dict.get("relationships", []) 32 relationships.extend( 33 parse_field_or_log_error(self.logger, relationship_dicts, self.parse_relationship, [], True) 34 ) 35 36 document_describes: List[str] = delete_duplicates_from_list(input_doc_dict.get("documentDescribes", [])) 37 doc_spdx_id: Optional[str] = input_doc_dict.get("SPDXID") 38 39 existing_relationships_without_comments: List[Relationship] = self.get_all_relationships_without_comments( 40 relationships 41 ) 42 relationships.extend( 43 parse_field_or_log_error( 44 self.logger, 45 document_describes, 46 lambda x: self.parse_document_describes( 47 doc_spdx_id=doc_spdx_id, 48 described_spdx_ids=x, 49 existing_relationships=existing_relationships_without_comments, 50 ), 51 [], 52 ) 53 ) 54 55 package_dicts: List[Dict] = input_doc_dict.get("packages", []) 56 existing_relationships_without_comments: List[Relationship] = self.get_all_relationships_without_comments( 57 relationships 58 ) 59 60 relationships.extend( 61 parse_field_or_log_error( 62 self.logger, 63 package_dicts, 64 lambda x: self.parse_has_files( 65 package_dicts=x, existing_relationships=existing_relationships_without_comments 66 ), 67 [], 68 ) 69 ) 70 71 file_dicts: List[Dict] = input_doc_dict.get("files", []) 72 73 # not implemented yet: deal with deprecated fields in file: 74 # https://github.com/spdx/tools-python/issues/294 & https://github.com/spdx/tools-python/issues/387 75 _ = self.parse_artifact_of(file_dicts=file_dicts) 76 _ = self.parse_file_dependencies(file_dicts=file_dicts) 77 78 raise_parsing_error_if_logger_has_messages(self.logger) 79 80 return relationships
def
parse_relationship( self, relationship_dict: dict) -> spdx_tools.spdx.model.relationship.Relationship:
82 def parse_relationship(self, relationship_dict: Dict) -> Relationship: 83 logger = Logger() 84 spdx_element_id: Optional[str] = relationship_dict.get("spdxElementId") 85 related_spdx_element: Optional[str] = parse_field_or_no_assertion_or_none( 86 relationship_dict.get("relatedSpdxElement") 87 ) 88 relationship_type: Optional[RelationshipType] = parse_field_or_log_error( 89 logger, relationship_dict.get("relationshipType"), self.parse_relationship_type 90 ) 91 relationship_comment: Optional[str] = relationship_dict.get("comment") 92 raise_parsing_error_if_logger_has_messages(logger, "Relationship") 93 94 relationship = construct_or_raise_parsing_error( 95 Relationship, 96 dict( 97 spdx_element_id=spdx_element_id, 98 relationship_type=relationship_type, 99 related_spdx_element_id=related_spdx_element, 100 comment=relationship_comment, 101 ), 102 ) 103 return relationship
@staticmethod
def
parse_relationship_type( relationship_type_str: str) -> spdx_tools.spdx.model.relationship.RelationshipType:
105 @staticmethod 106 def parse_relationship_type(relationship_type_str: str) -> RelationshipType: 107 try: 108 relationship_type = RelationshipType[json_str_to_enum_name(relationship_type_str)] 109 except KeyError: 110 raise SPDXParsingError([f"Invalid RelationshipType: {relationship_type_str}"]) 111 return relationship_type
def
parse_document_describes( self, doc_spdx_id: str, described_spdx_ids: list[str], existing_relationships: list[spdx_tools.spdx.model.relationship.Relationship]) -> list[spdx_tools.spdx.model.relationship.Relationship]:
113 def parse_document_describes( 114 self, doc_spdx_id: str, described_spdx_ids: List[str], existing_relationships: List[Relationship] 115 ) -> List[Relationship]: 116 logger = Logger() 117 describes_relationships = [] 118 for spdx_id in described_spdx_ids: 119 try: 120 describes_relationship = Relationship( 121 spdx_element_id=doc_spdx_id, 122 relationship_type=RelationshipType.DESCRIBES, 123 related_spdx_element_id=spdx_id, 124 ) 125 except ConstructorTypeErrors as err: 126 logger.append(err.get_messages()) 127 continue 128 if not self.check_if_relationship_exists(describes_relationship, existing_relationships): 129 describes_relationships.append(describes_relationship) 130 raise_parsing_error_if_logger_has_messages(logger, "document describes relationships") 131 132 return describes_relationships
def
parse_has_files( self, package_dicts: list[dict], existing_relationships: list[spdx_tools.spdx.model.relationship.Relationship]) -> list[spdx_tools.spdx.model.relationship.Relationship]:
134 def parse_has_files( 135 self, package_dicts: List[Dict], existing_relationships: List[Relationship] 136 ) -> List[Relationship]: 137 # assume existing relationships are stripped of comments 138 logger = Logger() 139 contains_relationships = [] 140 for package in package_dicts: 141 package_spdx_id: Optional[str] = package.get("SPDXID") 142 contained_files: List[str] = delete_duplicates_from_list(package.get("hasFiles", [])) 143 if not contained_files: 144 continue 145 for file_spdx_id in contained_files: 146 try: 147 contains_relationship = Relationship( 148 spdx_element_id=package_spdx_id, 149 relationship_type=RelationshipType.CONTAINS, 150 related_spdx_element_id=file_spdx_id, 151 ) 152 except ConstructorTypeErrors as err: 153 logger.append(err.get_messages()) 154 continue 155 if not self.check_if_relationship_exists( 156 relationship=contains_relationship, existing_relationships=existing_relationships 157 ): 158 contains_relationships.append(contains_relationship) 159 raise_parsing_error_if_logger_has_messages(logger, "package contains relationships") 160 161 return contains_relationships
def
check_if_relationship_exists( self, relationship: spdx_tools.spdx.model.relationship.Relationship, existing_relationships: list[spdx_tools.spdx.model.relationship.Relationship]) -> bool:
163 def check_if_relationship_exists( 164 self, relationship: Relationship, existing_relationships: List[Relationship] 165 ) -> bool: 166 # assume existing relationships are stripped of comments 167 if relationship in existing_relationships: 168 return True 169 relationship_inverted: Relationship = self.invert_relationship(relationship) 170 if relationship_inverted in existing_relationships: 171 return True 172 173 return False
@staticmethod
def
get_all_relationships_without_comments( existing_relationships: list[spdx_tools.spdx.model.relationship.Relationship]) -> list[spdx_tools.spdx.model.relationship.Relationship]:
175 @staticmethod 176 def get_all_relationships_without_comments(existing_relationships: List[Relationship]) -> List[Relationship]: 177 relationships_without_comments = [ 178 Relationship( 179 relationship_type=relationship.relationship_type, 180 related_spdx_element_id=relationship.related_spdx_element_id, 181 spdx_element_id=relationship.spdx_element_id, 182 ) 183 for relationship in existing_relationships 184 ] 185 return relationships_without_comments
def
invert_relationship( self, relationship: spdx_tools.spdx.model.relationship.Relationship) -> spdx_tools.spdx.model.relationship.Relationship:
187 def invert_relationship(self, relationship: Relationship) -> Relationship: 188 return Relationship( 189 related_spdx_element_id=relationship.spdx_element_id, 190 spdx_element_id=relationship.related_spdx_element_id, 191 relationship_type=self.invert_relationship_types[relationship.relationship_type], 192 comment=relationship.comment, 193 )
invert_relationship_types =
{<RelationshipType.DESCRIBES: 14>: <RelationshipType.DESCRIBED_BY: 13>, <RelationshipType.DESCRIBED_BY: 13>: <RelationshipType.DESCRIBES: 14>, <RelationshipType.CONTAINS: 6>: <RelationshipType.CONTAINED_BY: 5>, <RelationshipType.CONTAINED_BY: 5>: <RelationshipType.CONTAINS: 6>}
@staticmethod
def
parse_file_dependencies( file_dicts: list[dict]) -> list[spdx_tools.spdx.model.relationship.Relationship]:
202 @staticmethod 203 def parse_file_dependencies(file_dicts: List[Dict]) -> List[Relationship]: 204 dependency_relationships = [] 205 # the field fileDependencies is deprecated and should be converted to a relationship 206 # https://github.com/spdx/tools-python/issues/387 207 return dependency_relationships
@staticmethod
def
parse_artifact_of( file_dicts: list[dict]) -> list[spdx_tools.spdx.model.relationship.Relationship]:
209 @staticmethod 210 def parse_artifact_of(file_dicts: List[Dict]) -> List[Relationship]: 211 generated_relationships = [] 212 # artifactOfs is deprecated and should be converted to an external package and a generated from relationship 213 # https://github.com/spdx/tools-python/issues/294 214 return generated_relationships