@@ -157,15 +157,78 @@ def trim_version_report(version_report, reduced_path_set):
157157 reduced_aggregate_bom_view_entries = [e for e in aggregate_bom_view_entries if f"{ e ['producerProject' ]['id' ]} :{ e ['producerReleases' ][0 ]['id' ]} " in deduplicated ]
158158 version_report ['aggregateBomViewEntries' ] = reduced_aggregate_bom_view_entries
159159
160- def write_output_file (version_report , output_file ):
160+ '''
161+
162+ CSV output details
163+
164+ component name = aggregateBomViewEntries[].producerProject.name
165+ version name = aggregateBomViewEntries[].producerReleases[0].version
166+ license = licenses[].licenseDisplay
167+ file path = extract from detailedFileBomViewEntries
168+ match type = aggregateBomViewEntries[].matchTypes
169+ review status = aggregateBomViewEntries[].reviewSummary.reviewStatus
170+
171+ '''
172+ def get_csv_fieldnames ():
173+ return ['component name' , 'version name' , 'license' , 'match type' , 'review status' ]
174+
175+ def get_csv_data (version_report , keep_dupes ):
176+ csv_data = list ()
177+ components = list ()
178+ for bom_view_entry in version_report ['aggregateBomViewEntries' ]:
179+ entry = dict ()
180+ entry ['component name' ] = bom_view_entry ['producerProject' ]['name' ]
181+ entry ['version name' ] = bom_view_entry ['producerReleases' ][0 ]['version' ]
182+ entry ['license' ] = bom_view_entry ['licenses' ][0 ]['licenseDisplay' ].replace (' AND ' ,';' ).replace ('(' ,'' ).replace (')' ,'' )
183+ pid = bom_view_entry ['producerProject' ]['id' ]
184+ vid = bom_view_entry ['producerReleases' ][0 ]['id' ]
185+ #path_list = [p['path'] for p in version_report['detailedFileBomViewEntries'] if p['projectId'] == pid and p['versionId'] == vid]
186+ #entry['file path'] = ';'.join(path_list)
187+ entry ['match type' ] = ';' .join (bom_view_entry ['matchTypes' ])
188+ entry ['review status' ] = bom_view_entry ['reviewSummary' ]['reviewStatus' ]
189+
190+ # Only add if this component was not previously added.
191+ composite_key = pid + vid
192+ if composite_key not in components :
193+ csv_data .append (entry )
194+ components .append (composite_key )
195+ if keep_dupes :
196+ return csv_data
197+ else :
198+ return remove_duplicates (csv_data )
199+
200+ def remove_duplicates (data ):
201+ # Put data into buckets by version
202+ buckets = dict ()
203+ for row in data :
204+ name = row ['component name' ].lower ()
205+ version = row ['version name' ]
206+ if not version in buckets :
207+ buckets [version ] = [row ]
208+ else :
209+ buckets [version ].append (row )
210+ # Run reduction process for component names that start with existing component name
211+ # This process will ignore case in component names
212+ for set in buckets .values ():
213+ set .sort (key = lambda d : d ['component name' ].lower ())
214+ for row in set :
215+ index = set .index (row )
216+ name = row ['component name' ].lower ()
217+ while index + 1 < len (set ) and set [index + 1 ]['component name' ].lower ().startswith (name ):
218+ set .pop (index + 1 )
219+ reduced_data = list ()
220+ for b in buckets .values ():
221+ reduced_data .extend (b )
222+ return reduced_data
223+
224+ def write_output_file (version_report , output_file , keep_dupes ):
161225 if output_file .lower ().endswith (".csv" ):
162226 logging .info (f"Writing CSV output into { output_file } " )
163- field_names = list ( version_report [ 'aggregateBomViewEntries' ][ 0 ]. keys () )
227+ field_names = get_csv_fieldnames ( )
164228 with open (output_file , "w" ) as f :
165- writer = csv .DictWriter (f , fieldnames = field_names , extrasaction = 'ignore' ) # TODO
229+ writer = csv .DictWriter (f , fieldnames = field_names , extrasaction = 'ignore' , quoting = csv . QUOTE_ALL ) # TODO
166230 writer .writeheader ()
167- writer .writerows (version_report ['aggregateBomViewEntries' ])
168-
231+ writer .writerows (get_csv_data (version_report , keep_dupes ))
169232 return
170233 # If it's neither, then .json
171234 if not output_file .lower ().endswith (".json" ):
@@ -183,6 +246,7 @@ def parse_command_args():
183246 parser .add_argument ("-pn" , "--project-name" , required = True , help = "Project Name" )
184247 parser .add_argument ("-pv" , "--project-version-name" , required = True , help = "Project Version Name" )
185248 parser .add_argument ("-o" , "--output-file" , required = False , help = "File name to write output. File extension determines format .json and .csv, json is the default." )
249+ parser .add_argument ("-kd" , "--keep-dupes" , action = 'store_true' , help = "Do not reduce CVS data by fuzzy matching component names" )
186250 parser .add_argument ("-kh" , "--keep_hierarchy" , action = 'store_true' , help = "Set to keep all entries in the sources report. Will not remove components found under others." )
187251 parser .add_argument ("--report-retries" , metavar = "" , type = int , default = RETRY_LIMIT , help = "Retries for receiving the generated BlackDuck report. Generating copyright report tends to take longer minutes." )
188252 parser .add_argument ("--report-timeout" , metavar = "" , type = int , default = RETRY_TIMER , help = "Wait time between subsequent download attempts." )
@@ -230,7 +294,7 @@ def main():
230294 trim_version_report (version_report , reduced_path_set )
231295 logging .info (f"Truncated dataset contains { len (version_report ['aggregateBomViewEntries' ])} bom entries and { len (version_report ['detailedFileBomViewEntries' ])} file view entries" )
232296
233- write_output_file (version_report , output_file )
297+ write_output_file (version_report , output_file , args . keep_dupes )
234298
235299 # Combine component data with selected file data
236300 # Output result with CSV anf JSON as options.
0 commit comments