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 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.0.0
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 and
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 Python environment.
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 Codeartifact. It also provides an ``uv``
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=ogmaOdaXXZHaCYaPa0b2y11Rn6ZgMgzRftUG6-seBWM,36824
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.0.0.dist-info/licenses/LICENSE,sha256=4MAecetnRTQw5DlHtiikDSzKWO1xVLwzM5_DsPMYlnE,10172
18
- csspin_python-4.0.0.dist-info/METADATA,sha256=yC8s-jxM6RQjQWi6UsukDCZlGdoV_NOFubcZ0Y-QERM,5035
19
- csspin_python-4.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
20
- csspin_python-4.0.0.dist-info/top_level.txt,sha256=QSeglMEGbFu1z4L6MCQYwo01NgL0KojWvC4rzgMQ8gU,14
21
- csspin_python-4.0.0.dist-info/RECORD,,
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,,