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 ADDED
@@ -0,0 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul
2
+ # SPDX-FileType: SOURCE
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """Package information for Pitloom."""
6
+
7
+ __version__ = "0.1.0"
pitloom/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ # SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul
2
+ # SPDX-FileType: SOURCE
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """Pitloom: Creating SBOM during the build process."""
6
+
7
+ from pitloom.__about__ import __version__
8
+
9
+ __all__ = ["__version__"]
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,9 @@
1
+ # SPDX-FileCopyrightText: 2026-present Arthit Suriyawongkul
2
+ # SPDX-FileType: SOURCE
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """SPDX 3 SBOM assembler."""
6
+
7
+ from pitloom.assemble.spdx3.assembler import build
8
+
9
+ __all__ = ["build"]
@@ -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