csspin-python 4.0.0__py3-none-any.whl → 4.1.0rc1__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.
- csspin_python/python.py +28 -0
- csspin_python/python_sbom.py +196 -0
- csspin_python/python_sbom_schema.yaml +18 -0
- {csspin_python-4.0.0.dist-info → csspin_python-4.1.0rc1.dist-info}/METADATA +9 -5
- {csspin_python-4.0.0.dist-info → csspin_python-4.1.0rc1.dist-info}/RECORD +8 -6
- {csspin_python-4.0.0.dist-info → csspin_python-4.1.0rc1.dist-info}/WHEEL +0 -0
- {csspin_python-4.0.0.dist-info → csspin_python-4.1.0rc1.dist-info}/licenses/LICENSE +0 -0
- {csspin_python-4.0.0.dist-info → csspin_python-4.1.0rc1.dist-info}/top_level.txt +0 -0
csspin_python/python.py
CHANGED
|
@@ -69,12 +69,15 @@ point to the base installation.
|
|
|
69
69
|
import abc
|
|
70
70
|
import configparser
|
|
71
71
|
import hashlib
|
|
72
|
+
import json
|
|
72
73
|
import logging
|
|
73
74
|
import os
|
|
74
75
|
import re
|
|
75
76
|
import shutil
|
|
77
|
+
import subprocess
|
|
76
78
|
import sys
|
|
77
79
|
from contextlib import contextmanager
|
|
80
|
+
from functools import cache
|
|
78
81
|
from subprocess import CalledProcessError, check_output
|
|
79
82
|
from textwrap import dedent, indent
|
|
80
83
|
from typing import Generator, Iterable, Type, Union
|
|
@@ -88,6 +91,7 @@ except ImportError:
|
|
|
88
91
|
|
|
89
92
|
from click.exceptions import Abort
|
|
90
93
|
from csspin import (
|
|
94
|
+
CONFIG,
|
|
91
95
|
EXPORTS,
|
|
92
96
|
Command,
|
|
93
97
|
Memoizer,
|
|
@@ -716,6 +720,30 @@ class PythonActivate(ActivateScriptPatcher):
|
|
|
716
720
|
return value
|
|
717
721
|
|
|
718
722
|
|
|
723
|
+
@cache
|
|
724
|
+
def get_project_metadata(project_path: str, index_url: str) -> dict: # type: ignore[return] # pylint: disable=inconsistent-return-statements # noqa: E501
|
|
725
|
+
"""
|
|
726
|
+
Retrieve project metadata of ``project_path`` via ``python -m build
|
|
727
|
+
--metadata``.
|
|
728
|
+
|
|
729
|
+
Cached since ``build --metadata`` is noisy and the output is identical for
|
|
730
|
+
every caller within the same process.
|
|
731
|
+
"""
|
|
732
|
+
setenv(PIP_INDEX_URL=index_url)
|
|
733
|
+
kwargs = {}
|
|
734
|
+
if CONFIG.verbosity < Verbosity.INFO:
|
|
735
|
+
kwargs["stderr"] = subprocess.DEVNULL
|
|
736
|
+
raw_metadata = backtick(
|
|
737
|
+
"python", "-m", "build", "--metadata", project_path, **kwargs
|
|
738
|
+
)
|
|
739
|
+
setenv(PIP_INDEX_URL=None)
|
|
740
|
+
|
|
741
|
+
if raw_metadata:
|
|
742
|
+
return json.loads(raw_metadata) # type: ignore[no-any-return]
|
|
743
|
+
|
|
744
|
+
die(f"Could not retrieve project metadata of '{project_path}'.")
|
|
745
|
+
|
|
746
|
+
|
|
719
747
|
def get_site_packages(interpreter: Path) -> Path:
|
|
720
748
|
"""Return the path to the virtual environments site-packages."""
|
|
721
749
|
return Path(
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# -*- mode: python; coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2026 CONTACT Software GmbH
|
|
4
|
+
# https://www.contact-software.com/
|
|
5
|
+
#
|
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
# you may not use this file except in compliance with the License.
|
|
8
|
+
# You may obtain a copy of the License at
|
|
9
|
+
#
|
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
#
|
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
# See the License for the specific language governing permissions and
|
|
16
|
+
# limitations under the License.
|
|
17
|
+
|
|
18
|
+
"""Module implementing the python_sbom plugin for csspin"""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import sys
|
|
22
|
+
from subprocess import DEVNULL, PIPE
|
|
23
|
+
from tempfile import TemporaryDirectory
|
|
24
|
+
|
|
25
|
+
from csspin import (
|
|
26
|
+
Verbosity,
|
|
27
|
+
backtick,
|
|
28
|
+
config,
|
|
29
|
+
die,
|
|
30
|
+
exists,
|
|
31
|
+
info,
|
|
32
|
+
memoizer,
|
|
33
|
+
rmtree,
|
|
34
|
+
sh,
|
|
35
|
+
task,
|
|
36
|
+
)
|
|
37
|
+
from csspin.tree import ConfigTree
|
|
38
|
+
from packaging.requirements import Requirement
|
|
39
|
+
from path import Path
|
|
40
|
+
|
|
41
|
+
defaults = config(
|
|
42
|
+
cyclonedx_bom_version="7.3.0",
|
|
43
|
+
project_paths=["{spin.project_root}"],
|
|
44
|
+
requires=config(spin=["csspin_python.python"]),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@task("python-sbom", when="sbom:build")
|
|
49
|
+
def sbom(cfg: ConfigTree) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Create the SBOMs for Python projects defined in via
|
|
52
|
+
'python_sbom.project_paths'.
|
|
53
|
+
|
|
54
|
+
This task assumes that the current package defines its dependencies that
|
|
55
|
+
are to be included in the SBOM via the "thirdparty" extra.
|
|
56
|
+
|
|
57
|
+
If there is no such extra, the SBOM generation is skipped.
|
|
58
|
+
"""
|
|
59
|
+
from csspin_python.python import get_project_metadata
|
|
60
|
+
|
|
61
|
+
stderr = PIPE if cfg.verbosity > Verbosity.NORMAL else DEVNULL
|
|
62
|
+
for project_path in cfg.python_sbom.project_paths:
|
|
63
|
+
if not exists(project_path):
|
|
64
|
+
die(f"Project path '{project_path}' does not exist.")
|
|
65
|
+
|
|
66
|
+
project_path = Path(project_path).absolute()
|
|
67
|
+
metadata = get_project_metadata(project_path, cfg.python.index_url)
|
|
68
|
+
third_party_deps = _collect_thirdparty_deps(
|
|
69
|
+
metadata.get("requires_dist", set()), python_version=cfg.python.version
|
|
70
|
+
)
|
|
71
|
+
sbom_content = _run_cyclonedx(cfg, third_party_deps, stderr)
|
|
72
|
+
_write_sbom(cfg, sbom_content, metadata.get("name"), metadata.get("version"))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def cleanup(cfg: ConfigTree) -> None:
|
|
76
|
+
"""Get rid of all generated .cdx.json files and the cyclonedx-bom venv."""
|
|
77
|
+
for cdx_file in cfg.spin.project_root.glob("*.python_sbom.cdx.json"):
|
|
78
|
+
rmtree(cdx_file)
|
|
79
|
+
rmtree(cfg.spin.project_root / ".spin" / "venv_csspin_python__python_sbom")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---- Internals ---------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _ensure_cyclonedx_venv(cfg: ConfigTree, binary_dir: str, quiet: str | None) -> Path:
|
|
86
|
+
"""Return the cyclonedx-bom interpreter path, (re)creating the venv if needed.
|
|
87
|
+
|
|
88
|
+
We install cyclonedx-bom into a persistent venv since defining it as a
|
|
89
|
+
dependency of csspin-python itself doesn't work at this moment.
|
|
90
|
+
See https://github.com/CycloneDX/cyclonedx-python/issues/1045
|
|
91
|
+
"""
|
|
92
|
+
venv_cdx = cfg.spin.project_root / ".spin" / "venv_csspin_python__python_sbom"
|
|
93
|
+
interpreter_cdx = venv_cdx / binary_dir / "python" + cfg.platform.exe
|
|
94
|
+
|
|
95
|
+
requested_version = cfg.python_sbom.cyclonedx_bom_version
|
|
96
|
+
memo_key = f"cyclonedx-bom=={requested_version}"
|
|
97
|
+
memo_file = venv_cdx / "csspin_python_sbom.memo"
|
|
98
|
+
|
|
99
|
+
if venv_cdx.exists():
|
|
100
|
+
with memoizer(memo_file) as memo:
|
|
101
|
+
if memo.check(memo_key):
|
|
102
|
+
info(
|
|
103
|
+
f"Reusing existing cyclonedx-bom {requested_version} from {venv_cdx}"
|
|
104
|
+
)
|
|
105
|
+
return interpreter_cdx
|
|
106
|
+
info(
|
|
107
|
+
f"cyclonedx-bom version mismatch (wanted={requested_version}), "
|
|
108
|
+
f"recreating {venv_cdx}"
|
|
109
|
+
)
|
|
110
|
+
rmtree(venv_cdx)
|
|
111
|
+
|
|
112
|
+
sh(cfg.python.interpreter, "-m", "venv", venv_cdx)
|
|
113
|
+
sh(
|
|
114
|
+
interpreter_cdx,
|
|
115
|
+
"-m",
|
|
116
|
+
"pip",
|
|
117
|
+
quiet,
|
|
118
|
+
"--disable-pip-version-check",
|
|
119
|
+
"install",
|
|
120
|
+
"--index-url",
|
|
121
|
+
cfg.python.index_url,
|
|
122
|
+
"cyclonedx-bom==" + requested_version,
|
|
123
|
+
)
|
|
124
|
+
with memoizer(memo_file) as memo:
|
|
125
|
+
memo.add(memo_key)
|
|
126
|
+
return interpreter_cdx
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _run_cyclonedx(cfg: ConfigTree, third_party_deps: set[str], stderr: int) -> str:
|
|
130
|
+
"""
|
|
131
|
+
Install third-party deps into a temp venv and return the CycloneDX JSON.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
binary_dir = "Scripts" if sys.platform == "win32" else "bin"
|
|
135
|
+
quiet = None if cfg.verbosity > Verbosity.NORMAL else "-q"
|
|
136
|
+
interpreter_cdx = _ensure_cyclonedx_venv(cfg, binary_dir, quiet)
|
|
137
|
+
|
|
138
|
+
with TemporaryDirectory() as tmp_dir:
|
|
139
|
+
venv = Path(tmp_dir) / "venv"
|
|
140
|
+
interpreter = venv / binary_dir / "python" + cfg.platform.exe
|
|
141
|
+
sh(cfg.python.interpreter, "-m", "venv", venv)
|
|
142
|
+
if third_party_deps:
|
|
143
|
+
sh(
|
|
144
|
+
interpreter,
|
|
145
|
+
"-m",
|
|
146
|
+
"pip",
|
|
147
|
+
quiet,
|
|
148
|
+
"install",
|
|
149
|
+
"--index-url",
|
|
150
|
+
cfg.python.index_url,
|
|
151
|
+
*[
|
|
152
|
+
f"--constraint={constraint}"
|
|
153
|
+
for constraint in cfg.python.constraints
|
|
154
|
+
],
|
|
155
|
+
*third_party_deps,
|
|
156
|
+
stderr=stderr,
|
|
157
|
+
)
|
|
158
|
+
sh(interpreter, "-m", "pip", quiet, "uninstall", "-y", "pip")
|
|
159
|
+
return backtick(interpreter_cdx, "-m", "cyclonedx_py", "environment", venv, stderr=stderr) # type: ignore[no-any-return] # noqa: E501
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _write_sbom(
|
|
163
|
+
cfg: ConfigTree, content: str, project_name: str, project_version: str
|
|
164
|
+
) -> None:
|
|
165
|
+
"""Inject project metadata into the cyclonedx JSON and write the output file."""
|
|
166
|
+
output_file = cfg.spin.project_root / f"{project_name}.python_sbom.cdx.json"
|
|
167
|
+
# cyclonedx-bom doesn't add primary component name and version when not
|
|
168
|
+
# using pyproject.toml
|
|
169
|
+
sbom_json = json.loads(content)
|
|
170
|
+
sbom_json |= {
|
|
171
|
+
"metadata": {"component": {"name": project_name, "version": project_version}}
|
|
172
|
+
}
|
|
173
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
174
|
+
json.dump(sbom_json, f, indent=2, sort_keys=True)
|
|
175
|
+
info(f"Generated Python SBOM successfully ({output_file})")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _collect_thirdparty_deps(requires_dist: list, python_version: str) -> set[str]:
|
|
179
|
+
"""Extract 'thirdparty' dependency specifiers from project metadata."""
|
|
180
|
+
|
|
181
|
+
import platform
|
|
182
|
+
|
|
183
|
+
env = {
|
|
184
|
+
"sys_platform": sys.platform,
|
|
185
|
+
"extra": "thirdparty",
|
|
186
|
+
"platform_system": platform.system(),
|
|
187
|
+
"python_version": python_version,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
dependencies = set()
|
|
191
|
+
|
|
192
|
+
for require in requires_dist:
|
|
193
|
+
req = Requirement(require)
|
|
194
|
+
if req.marker and req.marker.evaluate(environment=env):
|
|
195
|
+
dependencies.add(req.name + str(req.specifier))
|
|
196
|
+
return dependencies
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# -*- mode: yaml; coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Schema for the python_sbom plugin for csspin
|
|
4
|
+
|
|
5
|
+
python_sbom:
|
|
6
|
+
type: object
|
|
7
|
+
help: Configuration related to the python_sbom plugin for csspin
|
|
8
|
+
properties:
|
|
9
|
+
project_paths:
|
|
10
|
+
type: list
|
|
11
|
+
help: |
|
|
12
|
+
List of paths to the Python projects for which to generate the
|
|
13
|
+
SBOM.
|
|
14
|
+
cyclonedx_version:
|
|
15
|
+
type: str
|
|
16
|
+
help: |
|
|
17
|
+
Version of the cyclonedx-bom package to use for generating the
|
|
18
|
+
SBOM.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: csspin-python
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.1.0rc1
|
|
4
4
|
Summary: Plugin-package for csspin providing Python related plugins
|
|
5
5
|
Author-email: CONTACT Software GmbH <info@contact-software.com>
|
|
6
6
|
Maintainer-email: Waleri Enns <waleri.enns@contact-software.com>, Benjamin Thomas Schwertfeger <benjaminthomas.schwertfeger@contact-software.com>, Fabian Hafer <fabian.hafer@contact-software.com>
|
|
@@ -45,18 +45,21 @@ The following plugins are available:
|
|
|
45
45
|
- `csspin_python.debugpy`: A plugin for debugging Python code using `debugpy`_.
|
|
46
46
|
- `csspin_python.devpi`: A plugin for simplified usage of `devpi`_.
|
|
47
47
|
- `csspin_python.pytest`: A plugin for running tests using pytest.
|
|
48
|
-
- `csspin_python.python`: A plugin for provisioning Python environments
|
|
49
|
-
installing dependencies.
|
|
48
|
+
- `csspin_python.python`: A plugin for provisioning Python environments,
|
|
49
|
+
installing dependencies, and managing the virtual environment.
|
|
50
|
+
- `csspin_python.python_sbom`: A plugin for generating a `CycloneDX`_ Software
|
|
51
|
+
Bill of Materials (SBOM) for Python third-party dependencies.
|
|
50
52
|
- `csspin_python.radon`: A plugin for running `radon`_ to analyze code
|
|
51
53
|
complexity.
|
|
52
54
|
- `csspin_python.sphinx`: A plugin for building Sphinx documentation.
|
|
53
55
|
- `csspin_python.playwright`: A plugin for running tests using `playwright`_.
|
|
54
56
|
This plugin is deprecated, use the pytest plugin with the
|
|
55
57
|
'pytest.playwright.enabled=true' setting instead.
|
|
56
|
-
- `csspin_python.uv_provisioner`: A plugin that uses `uv`_ to provision the
|
|
58
|
+
- `csspin_python.uv_provisioner`: A plugin that uses `uv`_ to provision the
|
|
59
|
+
Python environment.
|
|
57
60
|
|
|
58
61
|
The package provides an ``aws_auth`` extra, that, if enabled, can authenticate
|
|
59
|
-
to `CONTACT Software GmbH`_'s AWS
|
|
62
|
+
to `CONTACT Software GmbH`_'s AWS CodeArtifact. It also provides an ``uv``
|
|
60
63
|
extra, that is necessary for using the ``csspin_python.uv_provisioner`` plugin.
|
|
61
64
|
|
|
62
65
|
Prerequisites
|
|
@@ -120,4 +123,5 @@ tests using ``spin pytest`` and do other great things.
|
|
|
120
123
|
.. _`devpi`: https://pypi.org/project/devpi
|
|
121
124
|
.. _`playwright`: https://pypi.org/project/pytest-playwright
|
|
122
125
|
.. _`radon`: https://pypi.org/project/radon
|
|
126
|
+
.. _`CycloneDX`: https://cyclonedx.org/
|
|
123
127
|
.. _`uv`: https://docs.astral.sh/uv/
|
|
@@ -8,14 +8,16 @@ csspin_python/playwright.py,sha256=oFfphLqa4AB6K9vasCUFHN0kFXu63n3ocrsqVuRp4-0,5
|
|
|
8
8
|
csspin_python/playwright_schema.yaml,sha256=TSeR16YHa7m7bfO59F2eMV-jXcglluTJdEpUeL16saY,1178
|
|
9
9
|
csspin_python/pytest.py,sha256=N9YaU_ouQab0PFPf46HLE7Vg4JeoZW4dzVD7EevqJ1U,4573
|
|
10
10
|
csspin_python/pytest_schema.yaml,sha256=tzXtdF6MvGC9v59EVRJFfLeMMHqPsXcFXy2zJtRECBI,1535
|
|
11
|
-
csspin_python/python.py,sha256=
|
|
11
|
+
csspin_python/python.py,sha256=yqH1eG6mqRJomu-F99cB74PFaUzbSoG_dhpUjYwr1xE,37725
|
|
12
|
+
csspin_python/python_sbom.py,sha256=RpqobiJ768Q6no7uwYAqvykp1-Bj0bkvED3a3pZbEBI,6817
|
|
13
|
+
csspin_python/python_sbom_schema.yaml,sha256=VCxY9E2AG_fS00dvIYe6Fbvz1maPjpY0zyZK1Z0wJu4,538
|
|
12
14
|
csspin_python/python_schema.yaml,sha256=pgVVjByUYjxQWek7aFmjQzRwmq2ROLvHYgwGPMrT9sM,6351
|
|
13
15
|
csspin_python/radon.py,sha256=uFqm6FEi5oWj-_XVaAm3s9cam0cUmr1_FwRf40K6xWs,1876
|
|
14
16
|
csspin_python/radon_schema.yaml,sha256=rlRzXw5z4XbjOVznRiUxWGP4E9hx1Jm-gGw1iQiYzE0,548
|
|
15
17
|
csspin_python/uv_provisioner.py,sha256=1e-_Sb39JrqNWyaUNeBX59R5tutXLJ1ZsT7urCN1U0I,6044
|
|
16
18
|
csspin_python/uv_provisioner_schema.yaml,sha256=Y8ZNC2OMnhR8Us3WUXAXK9hMjqGWAKFJB2puX4X5XNQ,727
|
|
17
|
-
csspin_python-4.
|
|
18
|
-
csspin_python-4.
|
|
19
|
-
csspin_python-4.
|
|
20
|
-
csspin_python-4.
|
|
21
|
-
csspin_python-4.
|
|
19
|
+
csspin_python-4.1.0rc1.dist-info/licenses/LICENSE,sha256=4MAecetnRTQw5DlHtiikDSzKWO1xVLwzM5_DsPMYlnE,10172
|
|
20
|
+
csspin_python-4.1.0rc1.dist-info/METADATA,sha256=kW0dZXu1Wwuhw4EooWroAHOZGMw5wjBso1MFJDzkJX8,5257
|
|
21
|
+
csspin_python-4.1.0rc1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
22
|
+
csspin_python-4.1.0rc1.dist-info/top_level.txt,sha256=QSeglMEGbFu1z4L6MCQYwo01NgL0KojWvC4rzgMQ8gU,14
|
|
23
|
+
csspin_python-4.1.0rc1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|