22# SPDX-License-Identifier: Apache 2.0
33
44import argparse
5+ import logging
56import sys
7+ from importlib .metadata import PackageNotFoundError , version
68from pathlib import Path
79from typing import Dict , List , Union
810
911import pandas as pd
12+ import tqdm
1013
14+ from superbom .utils .logger import AppLogger
1115from superbom .utils .packageindexes .conda .condadependencies import CondaPackageUtil
1216from superbom .utils .packageindexes .pypi .pipdependencies import PyPIPackageUtil
13- from superbom .utils .logger import AppLogger
1417from superbom .utils .parsers import (
18+ extract_toml_dependencies ,
1519 parse_conda_env ,
1620 parse_poetry_toml ,
1721 parse_requirements ,
@@ -48,6 +52,11 @@ def save_results(results: Dict[str, pd.DataFrame], output_path: str, format: str
4852 try :
4953 with pd .ExcelWriter (output_path , engine = "openpyxl" , mode = "w" ) as writer :
5054 for sheet_name , df in results .items ():
55+ if df .empty :
56+ logger .warning (f"DataFrame for { sheet_name } is empty. Skipping." )
57+ continue
58+ # Ensure the sheet name is valid
59+ sheet_name = sheet_name [:31 ] # Excel sheet name limit
5160 df .to_excel (writer , sheet_name = sheet_name , index = False )
5261 except Exception as e :
5362 logger .error (f"Error writing to Excel file: { e } " )
@@ -62,6 +71,31 @@ def save_results(results: Dict[str, pd.DataFrame], output_path: str, format: str
6271 logger .info (f"License Info: { result } \n { df } " )
6372
6473
74+ def process_items (items , process_method , * args , ** kwargs ) -> List :
75+ """
76+ Process items using the specified method.
77+
78+ Args:
79+ items (list): List of items to process.
80+ process_method (callable): Method to process each item.
81+ *args: Additional arguments to pass to the process method.
82+ **kwargs: Additional keyword arguments to pass to the process method.
83+ """
84+ results = []
85+
86+ for item in tqdm .tqdm (
87+ items , desc = "Processing items" , unit = "item" , disable = logger .level > logging .INFO
88+ ):
89+ try :
90+ result = process_method (item , * args , ** kwargs )
91+ if result :
92+ results .append (result )
93+ except Exception as e :
94+ logger .error (f"Error processing item { item } : { e } " )
95+
96+ return results
97+
98+
6599def generatebom (args : argparse .ArgumentParser ):
66100 """
67101 Generates a Bill of Materials (BOM) from environment files.
@@ -73,6 +107,7 @@ def generatebom(args: argparse.ArgumentParser):
73107 - platform (str, optional): Platform for which to retrieve package information.
74108 - output (str, optional): Path to save the output file.
75109 - format (str, optional): Format of the output file (e.g., 'table', 'json').
110+ - version: Display the version of the package.
76111
77112 Returns:
78113 None: The function saves the BOM to the specified output path in the specified format.
@@ -102,46 +137,64 @@ def generatebom(args: argparse.ArgumentParser):
102137 if args .verbose :
103138 logger .setLevel ("DEBUG" )
104139
105- env_files = filter_by_extensions (args .path , ["yml" , "txt" , "toml" ])
140+ env_files = filter_by_extensions (args .path , ["yml" , "yaml" , " txt" , "toml" ])
106141
107142 packageutil = CondaPackageUtil ()
108143 pipdependencies = PyPIPackageUtil ()
109144
110- for env_file in env_files :
145+ for index , env_file in enumerate ( env_files ) :
111146 output_data = []
112147
113- if env_file .suffix .lower () == ".yml" :
148+ if env_file .suffix .lower () in [ ".yml" , ".yaml" ] and env_file . stem == "environment " :
114149 logger .info (f"Processing conda env file: { env_file } " )
115150 channels , conda_packages , pip_packages = parse_conda_env (env_file )
151+ if not conda_packages :
152+ logger .warning (f"No conda packages found in { env_file } . Skipping." )
153+ continue
154+ if not channels :
155+ logger .warning (f"No channels found in { env_file } . Skipping." )
156+ continue
157+ if not pip_packages :
158+ logger .warning (f"No pip packages found in { env_file } . Skipping." )
159+ continue
160+
116161 if args .platform :
117- packageutil ._cache .add_platform (args .platform )
162+ packageutil ._cache .platforms . append (args .platform )
118163
119164 if channels :
120- packageutil ._cache .add_channels (channels )
165+ for channel in channels :
166+ packageutil ._cache .add_channel (channel )
167+
121168 else :
122169 logger .warning ("No channels specified in environment file. Using defaults." )
123- packageutil ._cache .add_channels (packageutil ._cache .DEFAULT_CHANNELS )
170+ packageutil ._cache .add_channel (packageutil ._cache .DEFAULT_CHANNELS )
124171
125- conda_data = packageutil .retrieve_conda_package_info ( conda_packages )
126- conda_pip_data = pipdependencies . get_pip_packages_data ( pip_packages )
127- output_data . extend ( conda_data + conda_pip_data )
128- output_data = conda_data + conda_pip_data
172+ conda_data = process_items ( conda_packages , packageutil .retrieve_conda_package_info )
173+ output_data . extend ( conda_data )
174+ conda_pip_data = process_items ( pip_packages , pipdependencies . get_pip_package_data )
175+ output_data . extend ( conda_pip_data )
129176
130- elif env_file .suffix .lower () == ".txt" :
177+ elif env_file .suffix .lower () == ".txt" and env_file . stem == "requirements" :
131178 logger .info (f"Processing pip requirements file: { env_file } " )
132179 pip_packages = parse_requirements (env_file )
133- pip_data = pipdependencies . get_pip_packages_data (pip_packages )
180+ pip_data = process_items (pip_packages , pipdependencies . get_pip_package_data )
134181 output_data .extend (pip_data )
135182
136- elif env_file .suffix .lower () == ".toml" :
137- logger .info (f"Processing poetry file: { env_file } " )
183+ elif env_file .suffix .lower () == ".toml" and env_file . stem == "pyproject" :
184+ logger .info (f"Processing pyproject file: { env_file } " )
138185
139186 pip_packages = parse_poetry_toml (env_file )
140- pip_data = pipdependencies .get_pip_packages_data (pip_packages )
187+ if not pip_packages :
188+ pip_packages = extract_toml_dependencies (env_file )
189+ pip_data = process_items (pip_packages , pipdependencies .get_pip_package_data )
141190 output_data .extend (pip_data )
142191
143- df = pd .DataFrame (output_data )
144- results [env_file .name ] = df
192+ if output_data :
193+ df = pd .DataFrame (output_data )
194+ # use the parent directory name as the sheet name
195+ sheet_name = env_file .parent .name if env_file .parent .name else "default"
196+
197+ results [sheet_name ] = df
145198
146199 # Save results
147200 # output_path = args.output if args.output else 'bom.xlsx'
@@ -195,9 +248,18 @@ def main(argv=None):
195248 # Verbosity command
196249 parser .add_argument ("-v" , "--verbose" , action = "store_true" , help = "Enable verbose logging" )
197250
251+ # Version command
252+ parser .add_argument (
253+ "-V" ,
254+ "--version" ,
255+ action = "version" ,
256+ version = f"%(prog)s { version ('superbom' )} " ,
257+ help = "Show version and exit" ,
258+ )
259+
198260 args = parser .parse_args (argv )
199261 generatebom (args )
200262
201263
202- if __name__ == "__main__" : # pragma: no cover
264+ if __name__ == "__main__" : # pragma: no cover
203265 main ()
0 commit comments