pitloom 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pitloom/__about__.py +7 -0
- pitloom/__init__.py +9 -0
- pitloom/__main__.py +143 -0
- pitloom/assemble/__init__.py +62 -0
- pitloom/assemble/spdx3/__init__.py +9 -0
- pitloom/assemble/spdx3/assembler.py +158 -0
- pitloom/assemble/spdx3/deps.py +307 -0
- pitloom/assemble/spdx3/fragments.py +51 -0
- pitloom/bom.py +185 -0
- pitloom/core/__init__.py +5 -0
- pitloom/core/ai_metadata.py +58 -0
- pitloom/core/config.py +38 -0
- pitloom/core/creation.py +40 -0
- pitloom/core/document.py +40 -0
- pitloom/core/models.py +46 -0
- pitloom/core/project.py +41 -0
- pitloom/export/__init__.py +5 -0
- pitloom/export/spdx3_json.py +126 -0
- pitloom/extract/__init__.py +5 -0
- pitloom/extract/ai_model.py +443 -0
- pitloom/extract/pyproject.py +285 -0
- pitloom/plugins/__init__.py +5 -0
- pitloom/plugins/hatch.py +177 -0
- pitloom/py.typed +0 -0
- pitloom-0.1.0.dist-info/METADATA +408 -0
- pitloom-0.1.0.dist-info/RECORD +29 -0
- pitloom-0.1.0.dist-info/WHEEL +4 -0
- pitloom-0.1.0.dist-info/entry_points.txt +6 -0
- pitloom-0.1.0.dist-info/licenses/LICENSE +201 -0
pitloom/__about__.py
ADDED
pitloom/__init__.py
ADDED
pitloom/__main__.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul
|
|
2
|
+
# SPDX-FileType: SOURCE
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""Command-line interface for Pitloom SBOM generator."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import sys
|
|
11
|
+
import traceback
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from pitloom.__about__ import __version__
|
|
15
|
+
from pitloom.assemble import generate_sbom
|
|
16
|
+
from pitloom.core.creation import CreationMetadata
|
|
17
|
+
|
|
18
|
+
_SPDX3_JSON_EXT = ".spdx3.json"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _resolve_output_path(explicit: Path | None, project_dir: Path) -> Path:
|
|
22
|
+
"""Return the SBOM output path to use.
|
|
23
|
+
|
|
24
|
+
Priority:
|
|
25
|
+
1. Explicit ``-o`` / ``--output`` argument.
|
|
26
|
+
2. ``[tool.pitloom] sbom-basename`` from ``pyproject.toml``
|
|
27
|
+
→ ``<basename>.spdx3.json``.
|
|
28
|
+
3. ``<name>-<version>.spdx3.json`` derived from project metadata.
|
|
29
|
+
4. Fallback: ``sbom.spdx3.json``.
|
|
30
|
+
"""
|
|
31
|
+
if explicit is not None:
|
|
32
|
+
return explicit
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
# pylint: disable=import-outside-toplevel
|
|
36
|
+
from pitloom.extract.pyproject import read_pyproject
|
|
37
|
+
|
|
38
|
+
metadata, pitloom_config = read_pyproject(project_dir / "pyproject.toml")
|
|
39
|
+
|
|
40
|
+
if pitloom_config.sbom_basename:
|
|
41
|
+
return Path(f"{pitloom_config.sbom_basename}{_SPDX3_JSON_EXT}")
|
|
42
|
+
|
|
43
|
+
parts = [metadata.name] if metadata.name else ["sbom"]
|
|
44
|
+
if metadata.version:
|
|
45
|
+
parts.append(metadata.version)
|
|
46
|
+
return Path("-".join(parts) + _SPDX3_JSON_EXT)
|
|
47
|
+
|
|
48
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
49
|
+
return Path(f"sbom{_SPDX3_JSON_EXT}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def main() -> int:
|
|
53
|
+
"""Main entry point for the Pitloom CLI.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
int: Exit code (0 for success, 1 for error)
|
|
57
|
+
"""
|
|
58
|
+
parser = argparse.ArgumentParser(
|
|
59
|
+
description="Pitloom - Generate SPDX 3 SBOM for Python projects",
|
|
60
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"-V",
|
|
64
|
+
"--version",
|
|
65
|
+
action="version",
|
|
66
|
+
version=f"Pitloom {__version__}",
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"project_dir",
|
|
70
|
+
type=Path,
|
|
71
|
+
help="Path to the project directory (containing pyproject.toml)",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"-o",
|
|
75
|
+
"--output",
|
|
76
|
+
type=Path,
|
|
77
|
+
default=None,
|
|
78
|
+
help=(
|
|
79
|
+
"Output file path. "
|
|
80
|
+
"Default: <name>-<version>.spdx3.json derived from pyproject.toml, "
|
|
81
|
+
"or the basename from [tool.pitloom] sbom-basename if set."
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--creator-name",
|
|
86
|
+
type=str,
|
|
87
|
+
help="Name of the SBOM creator (default: Pitloom)",
|
|
88
|
+
)
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"--creator-email",
|
|
91
|
+
type=str,
|
|
92
|
+
help="Email of the SBOM creator",
|
|
93
|
+
)
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"--pretty",
|
|
96
|
+
action="store_true",
|
|
97
|
+
default=None,
|
|
98
|
+
help=(
|
|
99
|
+
"Pretty-print the SBOM output with 2-space indentation. "
|
|
100
|
+
"Overrides 'pretty' in [tool.pitloom] in pyproject.toml. "
|
|
101
|
+
"Default is compact output (machine-optimized)."
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
args = parser.parse_args()
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
project_dir = args.project_dir.resolve()
|
|
109
|
+
if not project_dir.exists():
|
|
110
|
+
print(f"Error: Project directory not found: {project_dir}", file=sys.stderr)
|
|
111
|
+
return 1
|
|
112
|
+
|
|
113
|
+
pyproject_path = project_dir / "pyproject.toml"
|
|
114
|
+
if not pyproject_path.exists():
|
|
115
|
+
print(
|
|
116
|
+
f"Error: pyproject.toml not found in {project_dir}",
|
|
117
|
+
file=sys.stderr,
|
|
118
|
+
)
|
|
119
|
+
return 1
|
|
120
|
+
|
|
121
|
+
output_path = _resolve_output_path(args.output, project_dir)
|
|
122
|
+
|
|
123
|
+
print(f"Generating SBOM for project in: {project_dir}")
|
|
124
|
+
generate_sbom(
|
|
125
|
+
project_dir,
|
|
126
|
+
output_path=output_path,
|
|
127
|
+
creation_info=CreationMetadata(
|
|
128
|
+
creator_name=args.creator_name or "Pitloom",
|
|
129
|
+
creator_email=args.creator_email or "",
|
|
130
|
+
),
|
|
131
|
+
pretty=args.pretty,
|
|
132
|
+
)
|
|
133
|
+
print(f"SBOM written to: {output_path}")
|
|
134
|
+
return 0
|
|
135
|
+
|
|
136
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
137
|
+
print(f"Error generating SBOM: {e}", file=sys.stderr)
|
|
138
|
+
traceback.print_exc()
|
|
139
|
+
return 1
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
sys.exit(main())
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
# SPDX-FileType: SOURCE
|
|
4
|
+
|
|
5
|
+
"""SBOM assemblers for different output specifications."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from pitloom.assemble.spdx3.assembler import build
|
|
12
|
+
from pitloom.assemble.spdx3.fragments import merge_fragments
|
|
13
|
+
from pitloom.core.creation import CreationMetadata
|
|
14
|
+
from pitloom.core.document import DocumentModel
|
|
15
|
+
from pitloom.extract.pyproject import read_pyproject
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def generate_sbom(
|
|
19
|
+
project_dir: Path,
|
|
20
|
+
output_path: Path | None = None,
|
|
21
|
+
creation_info: CreationMetadata | None = None,
|
|
22
|
+
pretty: bool | None = None,
|
|
23
|
+
) -> str:
|
|
24
|
+
"""Generate an SPDX 3 SBOM for a Python project.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
project_dir: Path to the project directory containing ``pyproject.toml``.
|
|
28
|
+
output_path: If given, the JSON-LD output is also written to this path.
|
|
29
|
+
pretty: If ``True``, indent the JSON output with 2 spaces.
|
|
30
|
+
If ``False``, produce compact output (no extra whitespace).
|
|
31
|
+
If ``None`` (default), read the setting from ``[tool.pitloom] pretty``
|
|
32
|
+
in ``pyproject.toml`` (which itself defaults to ``False``).
|
|
33
|
+
creation_info: Creator and timestamp metadata for the SBOM document.
|
|
34
|
+
When ``None`` a default :class:`~pitloom.core.creation.CreationMetadata`
|
|
35
|
+
is used (creator ``"Pitloom"``, current UTC time).
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
JSON-LD string of the generated SPDX 3 SBOM.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
FileNotFoundError: If ``pyproject.toml`` is not found in ``project_dir``.
|
|
42
|
+
ValueError: If required project metadata (e.g., ``name``) is missing.
|
|
43
|
+
"""
|
|
44
|
+
metadata, pitloom_config = read_pyproject(project_dir / "pyproject.toml")
|
|
45
|
+
effective_pretty: bool = pitloom_config.pretty if pretty is None else pretty
|
|
46
|
+
|
|
47
|
+
doc = DocumentModel(
|
|
48
|
+
project=metadata,
|
|
49
|
+
creation=creation_info or CreationMetadata(),
|
|
50
|
+
)
|
|
51
|
+
exporter = build(doc)
|
|
52
|
+
merge_fragments(project_dir, pitloom_config.fragments, exporter)
|
|
53
|
+
|
|
54
|
+
sbom_json = exporter.to_json(pretty=effective_pretty)
|
|
55
|
+
|
|
56
|
+
if output_path is not None:
|
|
57
|
+
output_path.write_text(sbom_json, encoding="utf-8")
|
|
58
|
+
|
|
59
|
+
return sbom_json
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
__all__ = ["generate_sbom"]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul
|
|
2
|
+
# SPDX-FileType: SOURCE
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""SPDX 3 assembler for Python projects."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from uuid import uuid4
|
|
11
|
+
|
|
12
|
+
from spdx_python_model import v3_0_1 as spdx3
|
|
13
|
+
|
|
14
|
+
from pitloom.assemble.spdx3.deps import add_dependencies, build_license_elements
|
|
15
|
+
from pitloom.core.document import DocumentModel
|
|
16
|
+
from pitloom.core.models import generate_spdx_id
|
|
17
|
+
from pitloom.export.spdx3_json import Spdx3JsonExporter
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build(doc: DocumentModel) -> Spdx3JsonExporter:
|
|
21
|
+
"""Assemble SPDX 3 elements from a :class:`~pitloom.core.document.DocumentModel`.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
doc: Format-neutral document model with project metadata, creation
|
|
25
|
+
metadata, and any AI model metadata.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
A populated :class:`~pitloom.export.spdx3_json.Spdx3JsonExporter`
|
|
29
|
+
containing all SPDX 3 elements for the project and its dependencies.
|
|
30
|
+
"""
|
|
31
|
+
metadata = doc.project
|
|
32
|
+
ci = doc.creation
|
|
33
|
+
created_at = (
|
|
34
|
+
datetime.fromisoformat(ci.creation_datetime)
|
|
35
|
+
if ci.creation_datetime
|
|
36
|
+
else datetime.now(timezone.utc)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
exporter = Spdx3JsonExporter()
|
|
40
|
+
doc_uuid = str(uuid4())
|
|
41
|
+
|
|
42
|
+
# --- Creation info, creator agent, and creation tool ---
|
|
43
|
+
spdx_ci = spdx3.CreationInfo(
|
|
44
|
+
specVersion="3.0.1",
|
|
45
|
+
created=created_at,
|
|
46
|
+
)
|
|
47
|
+
creator = spdx3.Person(
|
|
48
|
+
spdxId=generate_spdx_id("Person", doc_name=metadata.name, doc_uuid=doc_uuid),
|
|
49
|
+
name=ci.creator_name,
|
|
50
|
+
creationInfo=spdx_ci,
|
|
51
|
+
)
|
|
52
|
+
if ci.creator_email:
|
|
53
|
+
creator.externalIdentifier = [
|
|
54
|
+
spdx3.ExternalIdentifier(
|
|
55
|
+
externalIdentifierType=spdx3.ExternalIdentifierType.email,
|
|
56
|
+
identifier=ci.creator_email,
|
|
57
|
+
)
|
|
58
|
+
]
|
|
59
|
+
tool = spdx3.Tool(
|
|
60
|
+
spdxId=generate_spdx_id("Tool", doc_name=metadata.name, doc_uuid=doc_uuid),
|
|
61
|
+
name=ci.creation_tool,
|
|
62
|
+
creationInfo=spdx_ci,
|
|
63
|
+
)
|
|
64
|
+
spdx_ci.createdBy = [creator.spdxId]
|
|
65
|
+
spdx_ci.createdUsing = [tool.spdxId]
|
|
66
|
+
|
|
67
|
+
exporter.add_creation_info(spdx_ci)
|
|
68
|
+
exporter.add_person(creator)
|
|
69
|
+
exporter.object_set.add(tool)
|
|
70
|
+
|
|
71
|
+
# --- Main package ---
|
|
72
|
+
copyright_holder = (
|
|
73
|
+
metadata.authors[0].get("name", metadata.name)
|
|
74
|
+
if metadata.authors
|
|
75
|
+
else metadata.name
|
|
76
|
+
)
|
|
77
|
+
provenance_comment: str | None = None
|
|
78
|
+
if metadata.provenance:
|
|
79
|
+
parts = [f"{field}: {source}" for field, source in metadata.provenance.items()]
|
|
80
|
+
provenance_comment = "Metadata provenance: " + "; ".join(parts)
|
|
81
|
+
|
|
82
|
+
download_location = metadata.urls.get("Source") or metadata.urls.get("Homepage")
|
|
83
|
+
|
|
84
|
+
main_package = spdx3.software_Package(
|
|
85
|
+
spdxId=generate_spdx_id("Package", doc_name=metadata.name, doc_uuid=doc_uuid),
|
|
86
|
+
name=metadata.name,
|
|
87
|
+
creationInfo=spdx_ci,
|
|
88
|
+
)
|
|
89
|
+
main_package.software_packageVersion = metadata.version or "unknown"
|
|
90
|
+
main_package.suppliedBy = creator.spdxId
|
|
91
|
+
if metadata.description:
|
|
92
|
+
main_package.description = metadata.description
|
|
93
|
+
if download_location:
|
|
94
|
+
main_package.software_downloadLocation = download_location
|
|
95
|
+
if metadata.urls.get("Homepage"):
|
|
96
|
+
main_package.software_homePage = metadata.urls.get("Homepage")
|
|
97
|
+
main_package.software_copyrightText = (
|
|
98
|
+
f"Copyright (c) {datetime.now().year} {copyright_holder}"
|
|
99
|
+
)
|
|
100
|
+
main_package.software_primaryPurpose = spdx3.software_SoftwarePurpose.library
|
|
101
|
+
if ci.build_datetime:
|
|
102
|
+
main_package.builtTime = datetime.fromisoformat(ci.build_datetime)
|
|
103
|
+
if provenance_comment:
|
|
104
|
+
main_package.comment = provenance_comment
|
|
105
|
+
|
|
106
|
+
# --- SBOM and document envelope ---
|
|
107
|
+
sbom = spdx3.software_Sbom(
|
|
108
|
+
spdxId=generate_spdx_id("Sbom", doc_name=metadata.name, doc_uuid=doc_uuid),
|
|
109
|
+
creationInfo=spdx_ci,
|
|
110
|
+
rootElement=[main_package.spdxId],
|
|
111
|
+
)
|
|
112
|
+
sbom.software_sbomType = [spdx3.software_SbomType.build]
|
|
113
|
+
|
|
114
|
+
spdx_doc = spdx3.SpdxDocument(
|
|
115
|
+
spdxId=generate_spdx_id(
|
|
116
|
+
"SpdxDocument", doc_name=metadata.name, doc_uuid=doc_uuid
|
|
117
|
+
),
|
|
118
|
+
creationInfo=spdx_ci,
|
|
119
|
+
rootElement=[sbom.spdxId],
|
|
120
|
+
)
|
|
121
|
+
spdx_doc.profileConformance = [
|
|
122
|
+
spdx3.ProfileIdentifierType.core,
|
|
123
|
+
spdx3.ProfileIdentifierType.software,
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
exporter.add_document(spdx_doc)
|
|
127
|
+
exporter.add_sbom(sbom)
|
|
128
|
+
exporter.add_package(main_package)
|
|
129
|
+
|
|
130
|
+
# --- License ---
|
|
131
|
+
if metadata.license_name:
|
|
132
|
+
rel_declared, rel_concluded = build_license_elements(
|
|
133
|
+
license_id=metadata.license_name,
|
|
134
|
+
package_spdx_id=main_package.spdxId,
|
|
135
|
+
license_provenance=metadata.provenance.get(
|
|
136
|
+
"license", "Source: pyproject.toml | Field: project.license"
|
|
137
|
+
),
|
|
138
|
+
creation_info=spdx_ci,
|
|
139
|
+
doc_name=metadata.name,
|
|
140
|
+
doc_uuid=doc_uuid,
|
|
141
|
+
exporter=exporter,
|
|
142
|
+
)
|
|
143
|
+
exporter.add_relationship(rel_declared)
|
|
144
|
+
exporter.add_relationship(rel_concluded)
|
|
145
|
+
spdx_doc.profileConformance.append(spdx3.ProfileIdentifierType.simpleLicensing)
|
|
146
|
+
|
|
147
|
+
# --- Dependencies ---
|
|
148
|
+
add_dependencies(
|
|
149
|
+
dependencies=metadata.dependencies,
|
|
150
|
+
dep_provenance=metadata.provenance.get("dependencies", "Unknown source"),
|
|
151
|
+
main_package_spdx_id=main_package.spdxId,
|
|
152
|
+
creation_info=spdx_ci,
|
|
153
|
+
doc_name=metadata.name,
|
|
154
|
+
doc_uuid=doc_uuid,
|
|
155
|
+
exporter=exporter,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return exporter
|