Skip to content

Commit 851b98d

Browse files
Merge pull request #5 from IntelLabs/the-yaml-update
The yaml update
2 parents ef8f626 + d3c5b61 commit 851b98d

17 files changed

Lines changed: 825 additions & 312 deletions

File tree

.github/workflows/main.yml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# This workflow will install Python dependencies, run tests and lint with a single version of Python
2+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3+
4+
name: Unit Tests
5+
6+
on:
7+
push:
8+
branches: ["main"]
9+
pull_request:
10+
branches: ["main"]
11+
workflow_dispatch:
12+
13+
permissions:
14+
contents: read
15+
16+
jobs:
17+
unittest:
18+
runs-on: ubuntu-latest
19+
20+
steps:
21+
- name: Checkout repository
22+
uses: actions/checkout@v4
23+
24+
- name: Set up Python 3.13
25+
uses: actions/setup-python@v5.2.0
26+
with:
27+
python-version: "3.13"
28+
29+
- name: Cache Python virtual environment
30+
uses: actions/cache@v4
31+
with:
32+
path: ${{ github.workspace }}/.venv
33+
key: ${{ runner.os }}-python-${{ hashFiles('**/poetry.lock') }}
34+
restore-keys: |
35+
${{ runner.os }}-python-${{ hashFiles('**/poetry.lock') }}
36+
${{ runner.os }}-python-
37+
38+
- name: Install Poetry
39+
run: |
40+
python3 -m pip install --upgrade pip
41+
python3 -m pip install poetry
42+
43+
- name: Configure Poetry to create virtual environment in project directory
44+
run: |
45+
poetry config virtualenvs.in-project true
46+
47+
- name: Install dependencies
48+
run: |
49+
poetry install
50+
poetry env info
51+
52+
- name: Run tests
53+
run: |
54+
source .venv/bin/activate
55+
poetry run pytest
56+
57+
- name: Upload coverage report
58+
if: ${{ !env.ACT }}
59+
uses: actions/upload-artifact@v4.4.0
60+
with:
61+
name: coverage-report
62+
path: |
63+
coverage.xml

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,8 @@ tests/fuzzer
77
tmp
88
*.pyc
99
.coverage
10+
coverage.xml
11+
*.xlsx
12+
*.xls
13+
*.csv
14+
*.json

.vscode/settings.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
"-p",
77
"test_*.py"
88
],
9-
"python.testing.pytestEnabled": false,
10-
"python.testing.unittestEnabled": true,
11-
}
9+
"python.testing.pytestEnabled": true,
10+
"python.testing.unittestEnabled": false,
11+
"python.testing.pytestArgs": [
12+
"tests"
13+
],
14+
}

poetry.lock

Lines changed: 154 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "superBOM"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
description = ""
55
authors = ["Michael Beale <michael.beale@intel.com>"]
66
license = "Apache-2.0"
@@ -22,6 +22,9 @@ foss-flame = "^0.20.7"
2222
colorlog = "^6.9.0"
2323
poetry-core = "^1.9.1"
2424
packaging = "^24.2"
25+
pytest = "^8.3.4"
26+
pytest-cov = "^6.0.0"
27+
tomli = "^2.2.1"
2528

2629
[tool.poetry.scripts]
2730
superbom = "superbom.main:main"

pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[pytest]
2+
addopts = --cov=superbom --cov-report=xml --cov-fail-under=85

src/superbom/main.py

Lines changed: 81 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22
# SPDX-License-Identifier: Apache 2.0
33

44
import argparse
5+
import logging
56
import sys
7+
from importlib.metadata import PackageNotFoundError, version
68
from pathlib import Path
79
from typing import Dict, List, Union
810

911
import pandas as pd
12+
import tqdm
1013

14+
from superbom.utils.logger import AppLogger
1115
from superbom.utils.packageindexes.conda.condadependencies import CondaPackageUtil
1216
from superbom.utils.packageindexes.pypi.pipdependencies import PyPIPackageUtil
13-
from superbom.utils.logger import AppLogger
1417
from 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+
6599
def 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()

src/superbom/utils/logger.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import colorlog
88

9+
910
class AppLogger:
1011
_instance = None
1112
_initialized = False
@@ -48,3 +49,6 @@ def _setup_logger(self):
4849

4950
def get_logger(self):
5051
return self.logger
52+
53+
def get_level(self):
54+
return self.logger.level

src/superbom/utils/packageindexes/conda/condacache.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ def __init__(self):
2626
self._cache_dir = Path.joinpath(Path.home(), ".cbomcache")
2727

2828
self.caches = {}
29-
self._platforms:List[str] = self.DEFAULT_PLATFORMS
30-
self._channels:List[str] = self.DEFAULT_CHANNELS
29+
self._platforms: List[str] = self.DEFAULT_PLATFORMS
30+
self._channels: List[str] = self.DEFAULT_CHANNELS
3131

3232
def add_cache(self, channel, platform):
3333
data = self.get_cached_data(channel, platform)
@@ -61,7 +61,7 @@ def platforms(self):
6161
return self._platforms
6262

6363
@platforms.setter
64-
def platforms(self, value:str):
64+
def platforms(self, value: str):
6565
if not isinstance(value, str):
6666
raise TypeError("Platform must be a string")
6767

@@ -71,18 +71,16 @@ def platforms(self, value:str):
7171
@property
7272
def channels(self):
7373
return self._channels
74-
75-
@channels.setter
76-
def channels(self, value: str) -> List[str]:
74+
75+
def add_channel(self, value: str):
76+
7777
if not isinstance(value, str):
7878
raise TypeError("Channel must be a string")
79-
79+
8080
if value in self.BANNED_CHANNELS:
81-
logger.warning(
82-
"Warning - Skipping Anaconda channels."
83-
)
84-
elif value not in self.channels:
85-
self.channels.append(value)
81+
logger.warning("Warning - Skipping Anaconda channels.")
82+
elif value not in self._channels:
83+
self._channels.append(value)
8684

8785
return self._channels
8886

@@ -168,6 +166,6 @@ def update_cache(self):
168166
logger.debug(f"Data for {channel}/{platform} already cached")
169167

170168

171-
if __name__ == "__main__": # pragma: no cover
169+
if __name__ == "__main__": # pragma: no cover
172170
cache = CondaCache()
173171
cache.update_cache()

0 commit comments

Comments
 (0)