csspin-python 5.0.0__py3-none-any.whl → 6.0.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.
csspin_python/python.py CHANGED
@@ -81,6 +81,8 @@ from functools import cache
81
81
  from subprocess import CalledProcessError, check_output
82
82
  from textwrap import dedent, indent
83
83
  from typing import Generator, Iterable, Type, Union
84
+ from urllib.error import HTTPError
85
+ from urllib.parse import urljoin, urlsplit
84
86
 
85
87
  try:
86
88
  from typing import Self # type: ignore[attr-defined]
@@ -162,11 +164,6 @@ defaults = config(
162
164
  enabled=False,
163
165
  memo="{spin.spin_dir}/aws_auth.memo",
164
166
  key_duration=3600 * 10, # 10 hours
165
- static_oidc=False,
166
- index="16.0/simple",
167
- # Need to set client secret to empty string, otherwise the string "None"
168
- # would be handled as secret and obfucsated in logs.
169
- client_secret="", # nosec: B106
170
167
  ),
171
168
  index_url="https://pypi.org/simple",
172
169
  skip_js_build=None,
@@ -187,6 +184,8 @@ def wheel(
187
184
  ) -> None:
188
185
  """Build a wheel of the current project and any additional wheels."""
189
186
  setenv(PIP_INDEX_URL=cfg.python.index_url)
187
+ if cfg.python.extra_index_urls:
188
+ setenv(PIP_EXTRA_INDEX_URL=" ".join(cfg.python.extra_index_urls))
190
189
  search_paths = paths or cfg.python.build_wheels
191
190
  for build_path in {Path(path).absolute() for path in search_paths}:
192
191
  try:
@@ -792,7 +791,9 @@ class PythonActivate(ActivateScriptPatcher):
792
791
 
793
792
 
794
793
  @cache
795
- def get_project_metadata(project_path: str, index_url: str) -> dict: # type: ignore[return] # pylint: disable=inconsistent-return-statements # noqa: E501
794
+ def get_project_metadata( # type: ignore[return] # pylint: disable=inconsistent-return-statements # noqa: E501
795
+ project_path: str, index_url: str, extra_index_urls: tuple[str, ...] = ()
796
+ ) -> dict:
796
797
  """
797
798
  Retrieve project metadata of ``project_path`` via ``python -m build
798
799
  --metadata``.
@@ -801,12 +802,16 @@ def get_project_metadata(project_path: str, index_url: str) -> dict: # type: ig
801
802
  every caller within the same process.
802
803
  """
803
804
  setenv(PIP_INDEX_URL=index_url)
805
+ if extra_index_urls:
806
+ setenv(PIP_EXTRA_INDEX_URL=" ".join(extra_index_urls))
804
807
  kwargs = {}
805
808
  if CONFIG.verbosity < Verbosity.INFO:
806
809
  kwargs["stderr"] = subprocess.DEVNULL
807
810
  raw_metadata = backtick(
808
811
  "python", "-m", "build", "--metadata", project_path, **kwargs
809
812
  )
813
+ if extra_index_urls:
814
+ setenv(PIP_EXTRA_INDEX_URL=None)
810
815
  setenv(PIP_INDEX_URL=None)
811
816
 
812
817
  if raw_metadata:
@@ -1125,21 +1130,78 @@ def _configure_pipconf(cfg: ConfigTree, update: bool = False) -> None:
1125
1130
  "index_url" in config_parser["global"] or "index-url" in config_parser["global"]
1126
1131
  ):
1127
1132
  config_parser["global"]["index_url"] = interpolate1(cfg.python.index_url)
1133
+
1134
+ # Extra index urls are only configured via 'python.extra_index_urls', not
1135
+ # 'python.pipconf', so drop whatever the pipconf template declares rather
1136
+ # than merging it in.
1137
+ for key in ("extra_index_url", "extra-index-url"):
1138
+ config_parser["global"].pop(key, None)
1139
+
1140
+ if cfg.python.extra_index_urls:
1141
+ config_parser["global"]["extra_index_url"] = "\n".join(
1142
+ interpolate1(url) for url in cfg.python.extra_index_urls
1143
+ )
1128
1144
  with open(_get_pipconf(cfg), mode="w", encoding="utf-8") as fd:
1129
1145
  config_parser.write(fd)
1130
1146
 
1131
1147
 
1132
1148
  def _obfuscate_index_url(index_url: str) -> None:
1133
- """Add the CodeArtifact token to the secrets."""
1149
+ """Add the credentials of an index URL to the secrets."""
1134
1150
 
1135
1151
  from csspin import secrets
1136
1152
 
1137
- secrets.add(index_url.split(":")[2].split("@")[0]) # Codeartifact token
1153
+ if password := urlsplit(index_url).password:
1154
+ secrets.add(password)
1138
1155
 
1139
1156
 
1140
- def _check_aws_token_validity( # pylint: disable=too-many-locals
1141
- cfg: ConfigTree,
1142
- ) -> None:
1157
+ def _merge_extra_index_urls(existing: Iterable[str], new: list[str]) -> list[str]:
1158
+ """
1159
+ Merge `new` extra index URLs into `existing`, preserving order and dropping
1160
+ duplicates. Used so that a user-configured ``python.extra_index_urls``
1161
+ survives aws_auth resolving its own CodeArtifact-backed extra indexes.
1162
+ """
1163
+ merged = list(existing)
1164
+ for url in new:
1165
+ if url not in merged:
1166
+ merged.append(url)
1167
+ return merged
1168
+
1169
+
1170
+ def _resolve_extra_index_urls(cfg: ConfigTree, index_base_url: str) -> list[str]:
1171
+ """
1172
+ Resolve ``python.aws_auth.extra_indexes`` into full, authenticated
1173
+ CodeArtifact index URLs, obfuscating each token along the way.
1174
+ """
1175
+
1176
+ extra_index_urls = []
1177
+ for extra_index in cfg.python.aws_auth.extra_indexes:
1178
+ extra_index_url = urljoin(index_base_url + "/", interpolate1(extra_index))
1179
+ # Kinda redundant since index_url carries the same secret and is already
1180
+ # obfuscated. So this noop here just exists to make the obfucation
1181
+ # explicit.
1182
+ _obfuscate_index_url(extra_index_url)
1183
+ extra_index_urls.append(extra_index_url)
1184
+ return extra_index_urls
1185
+
1186
+
1187
+ def _apply_resolved_index_urls(cfg: ConfigTree, index_base_url: str) -> None:
1188
+ """
1189
+ Set ``python.index_url`` and merge-resolve ``python.extra_index_urls`` from
1190
+ `index_base_url`, the CodeArtifact domain/repository base shared by the
1191
+ primary index and any ``aws_auth.extra_indexes``. Shared by both the
1192
+ fresh-token and cached-token paths of ``_check_aws_token_validity``.
1193
+ """
1194
+
1195
+ index_url = urljoin(index_base_url + "/", interpolate1(cfg.python.aws_auth.index))
1196
+ cfg.python.index_url = index_url
1197
+ _obfuscate_index_url(index_url)
1198
+ cfg.python.extra_index_urls = _merge_extra_index_urls(
1199
+ cfg.python.extra_index_urls,
1200
+ _resolve_extra_index_urls(cfg, index_base_url),
1201
+ )
1202
+
1203
+
1204
+ def _check_aws_token_validity(cfg: ConfigTree) -> None:
1143
1205
  """
1144
1206
  If csspin-python[aws_auth] is installed, we can use csaccess to get the
1145
1207
  CodeArtifact authentication token.
@@ -1155,65 +1217,39 @@ def _check_aws_token_validity( # pylint: disable=too-many-locals
1155
1217
 
1156
1218
  import time
1157
1219
 
1158
- client_secret = interpolate1(cfg.python.aws_auth.client_secret) or os.getenv(
1159
- "CS_AWS_OIDC_CLIENT_SECRET"
1160
- )
1161
- static_oidc = interpolate1(cfg.python.aws_auth.static_oidc).lower() == "true"
1162
-
1163
- if static_oidc and not client_secret:
1164
- die(
1165
- "Please provide a client secret for CodeArtifact access via"
1166
- " 'python.aws_auth.client_secret' when using static OIDC."
1167
- )
1168
-
1169
1220
  current_time = int(time.time())
1170
1221
  timestamp_key = "aws_auth_timestamp"
1171
1222
 
1172
1223
  with memoizer(cfg.python.aws_auth.memo) as memo:
1173
- for item in memo.items():
1174
- if isinstance(item, str) and item.startswith(f"{timestamp_key}:"):
1175
- last_time = int(item.split(":", 1)[1])
1224
+ if memo.items() and (item := memo.items()[0]):
1225
+ if not isinstance(item, tuple):
1226
+ memo.clear()
1227
+ else:
1228
+ _, last_time, index_base_url = item
1176
1229
  if current_time - last_time < int(
1177
1230
  interpolate1(cfg.python.aws_auth.key_duration)
1178
1231
  ):
1179
- pipconf = _get_pipconf(cfg)
1180
- config_parser = configparser.ConfigParser()
1181
- config_parser.read(pipconf)
1182
- info(f"Using existing index URL from {pipconf}.")
1183
-
1184
- if index_url := (
1185
- config_parser["global"].get("index_url")
1186
- or config_parser["global"].get("index-url")
1187
- ):
1188
- cfg.python.index_url = index_url
1189
- _obfuscate_index_url(index_url)
1190
- break
1191
- memo.items().remove(item)
1192
- else:
1193
- info("Updating Codeartifact token.")
1194
- from urllib.error import HTTPError
1195
- from urllib.parse import urljoin
1196
-
1197
- opts = {
1198
- "client_secret": client_secret,
1199
- "static_oidc": static_oidc,
1200
- }
1201
- if cfg.python.aws_auth.client_id:
1202
- opts["client_id"] = interpolate1(cfg.python.aws_auth.client_id)
1203
- if cfg.python.aws_auth.role_arn:
1204
- opts["aws_role_arn"] = interpolate1(cfg.python.aws_auth.role_arn)
1232
+ info("Using cached CodeArtifact token.")
1233
+ _apply_resolved_index_urls(cfg, index_base_url)
1234
+ return
1235
+ memo.clear()
1205
1236
 
1206
- try:
1207
- index_base_url = get_ca_pypi_url_programmatic(**opts)
1208
- except HTTPError as e:
1209
- die(f"Failed to establish CodeArtifact connection: {e}")
1237
+ info("Updating Codeartifact token.")
1210
1238
 
1211
- index_url = urljoin(
1212
- index_base_url + "/", interpolate1(cfg.python.aws_auth.index)
1213
- )
1214
- cfg.python.index_url = index_url
1215
- _obfuscate_index_url(index_url)
1239
+ opts = {}
1240
+
1241
+ if cfg.python.aws_auth.client_id:
1242
+ opts["client_id"] = interpolate1(cfg.python.aws_auth.client_id)
1243
+ if cfg.python.aws_auth.role_arn:
1244
+ opts["aws_role_arn"] = interpolate1(cfg.python.aws_auth.role_arn)
1245
+
1246
+ try:
1247
+ index_base_url = get_ca_pypi_url_programmatic(**opts)
1248
+ except HTTPError as e:
1249
+ die(f"Failed to establish CodeArtifact connection: {e}")
1250
+
1251
+ _apply_resolved_index_urls(cfg, index_base_url)
1216
1252
 
1217
- if exists(cfg.python.venv):
1218
- _configure_pipconf(cfg, update=True)
1219
- memo.add(f"{timestamp_key}:{current_time}")
1253
+ if exists(cfg.python.venv):
1254
+ _configure_pipconf(cfg, update=True)
1255
+ memo.add((timestamp_key, current_time, index_base_url))
@@ -71,7 +71,9 @@ def sbom(cfg: ConfigTree) -> None:
71
71
  die(f"Project path '{project_path}' does not exist.")
72
72
 
73
73
  project_path = Path(project_path).absolute()
74
- metadata = get_project_metadata(project_path, cfg.python.index_url)
74
+ metadata = get_project_metadata(
75
+ project_path, cfg.python.index_url, tuple(cfg.python.extra_index_urls)
76
+ )
75
77
  project_name, project_version = metadata.get("name"), metadata.get("version")
76
78
 
77
79
  file_name = _predict_wheel_filename(
@@ -138,6 +140,7 @@ def _ensure_cyclonedx_venv(cfg: ConfigTree, binary_dir: str, quiet: str | None)
138
140
  "install",
139
141
  "--index-url",
140
142
  cfg.python.index_url,
143
+ *[f"--extra-index-url={url}" for url in cfg.python.extra_index_urls],
141
144
  "cyclonedx-bom==" + requested_version,
142
145
  use_subprocess_environment=False,
143
146
  )
@@ -168,6 +171,7 @@ def _run_cyclonedx(cfg: ConfigTree, third_party_deps: set[str], stderr: int) ->
168
171
  "install",
169
172
  "--index-url",
170
173
  cfg.python.index_url,
174
+ *[f"--extra-index-url={url}" for url in cfg.python.extra_index_urls],
171
175
  *[
172
176
  f"--constraint={constraint}"
173
177
  for constraint in cfg.python.constraints
@@ -116,6 +116,13 @@ python:
116
116
  The index url from where packages should get installed into the
117
117
  venv. This value will also be put into cfg.python.pipconf, if
118
118
  not already mentioned there.
119
+ extra_index_urls:
120
+ type: list
121
+ help: |
122
+ Additional package indexes to install from, alongside
123
+ 'index_url'. If 'aws_auth.extra_indexes' is set, its
124
+ resolved, authenticated CodeArtifact URLs are merged into
125
+ this list.
119
126
  build_wheels:
120
127
  type: list
121
128
  help: |
@@ -141,22 +148,17 @@ python:
141
148
  Time in seconds defining how long the plugin should
142
149
  consider the authentication token as valid before
143
150
  issuing a new one.
144
- static_oidc:
145
- type: bool
146
- help: |
147
- Whether to static OIDC when authenticating with AWS
148
- CodeArtifact.
149
151
  index:
150
152
  type: str
151
- help: The CodeArtifact repository index (e.g. "16.0/simple").
153
+ help: The CodeArtifact repository index (e.g. "2026.2/simple").
154
+ extra_indexes:
155
+ type: list
156
+ help: |
157
+ Additional CodeArtifact repository indexes to
158
+ authenticate against, alongside 'index'.
152
159
  client_id:
153
160
  type: str
154
161
  help: The OIDC client ID to use.
155
- client_secret:
156
- type: secret
157
- help: |
158
- The OIDC client secret to use, defaults to the
159
- environment variable 'CS_AWS_OIDC_CLIENT_SECRET'.
160
162
  role_arn:
161
163
  type: str
162
164
  help: The role ARN to assume when authenticating.
@@ -161,16 +161,26 @@ class SimpleUvProvisioner(SimpleProvisioner):
161
161
  self._uv_cmd("pip", "install", "pip")
162
162
 
163
163
 
164
+ def _set_extra_index_url(toml_content: dict, extra_index_urls: list[str]) -> None:
165
+ """Set or drop the 'extra-index-url' key in `toml_content` in place."""
166
+ if extra_index_urls:
167
+ toml_content["extra-index-url"] = extra_index_urls
168
+ else:
169
+ toml_content.pop("extra-index-url", None)
170
+
171
+
164
172
  def _configure_uv_toml(cfg: ConfigTree) -> None:
165
173
  """
166
174
  Create a config file for uv, similar to the pip.conf of
167
175
  csspin_python.python, since `uv` pip won't respect the pip.conf.
176
+
177
+ 'index-url'/'extra-index-url' declared in the 'uv_provisioner.uv_toml'
178
+ template are ignored; 'python.index_url'/'python.extra_index_urls' are
179
+ the only supported way to configure these.
168
180
  """
169
181
  toml_content = tomllib.loads(cfg.uv_provisioner.uv_toml or "")
170
- if "index-url" not in toml_content:
171
- toml_content["index-url"] = cfg.python.index_url
172
- else:
173
- toml_content["index-url"] = toml_content.get("index-url", cfg.python.index_url)
182
+ toml_content["index-url"] = cfg.python.index_url
183
+ _set_extra_index_url(toml_content, cfg.python.extra_index_urls)
174
184
 
175
185
  with open(cfg.uv_provisioner.uv_toml_path, mode="wb") as fd:
176
186
  tomli_w.dump(toml_content, fd)
@@ -178,12 +188,20 @@ def _configure_uv_toml(cfg: ConfigTree) -> None:
178
188
 
179
189
  def _update_index_url_in_toml(cfg: ConfigTree) -> None:
180
190
  """
181
- Update the index-url in the uv.toml in case it changed.
191
+ Update the index-url and extra-index-url in the uv.toml in case they
192
+ changed.
182
193
  """
183
194
  if (uv_toml_path := interpolate1(Path(cfg.uv_provisioner.uv_toml_path))).exists():
184
195
  with open(uv_toml_path, mode="rb") as fd:
185
196
  toml_content = tomllib.load(fd)
197
+ changed = False
186
198
  if toml_content.get("index-url") != cfg.python.index_url:
187
199
  toml_content["index-url"] = cfg.python.index_url
200
+ changed = True
201
+
202
+ if toml_content.get("extra-index-url", []) != cfg.python.extra_index_urls:
203
+ _set_extra_index_url(toml_content, cfg.python.extra_index_urls)
204
+ changed = True
205
+ if changed:
188
206
  with open(uv_toml_path, mode="wb") as fd:
189
207
  tomli_w.dump(toml_content, fd)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csspin-python
3
- Version: 5.0.0
3
+ Version: 6.0.0
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>
@@ -27,7 +27,7 @@ License-File: LICENSE
27
27
  Requires-Dist: platformdirs~=4.3.8
28
28
  Requires-Dist: virtualenv
29
29
  Provides-Extra: aws-auth
30
- Requires-Dist: csaccess>=0.1.0; extra == "aws-auth"
30
+ Requires-Dist: csaccess>=0.3.0; extra == "aws-auth"
31
31
  Provides-Extra: uv
32
32
  Requires-Dist: tomli; python_version < "3.11" and extra == "uv"
33
33
  Requires-Dist: tomli-w; extra == "uv"
@@ -9,16 +9,16 @@ csspin_python/playwright.py,sha256=oFfphLqa4AB6K9vasCUFHN0kFXu63n3ocrsqVuRp4-0,5
9
9
  csspin_python/playwright_schema.yaml,sha256=TSeR16YHa7m7bfO59F2eMV-jXcglluTJdEpUeL16saY,1178
10
10
  csspin_python/pytest.py,sha256=vSLtmS-kNO0NPo_tszfEm5zGwJb6Yb5IGS-hA7CNguo,4544
11
11
  csspin_python/pytest_schema.yaml,sha256=tzXtdF6MvGC9v59EVRJFfLeMMHqPsXcFXy2zJtRECBI,1535
12
- csspin_python/python.py,sha256=SO5RkNdpZPcux7KnuDHVTaNIVQAmU9xjwTtJG4v97NE,40725
13
- csspin_python/python_sbom.py,sha256=FYx9DMDSeooB4lXuIisUsMwBMEj1Hl-QVl1AlDdTmZk,13381
12
+ csspin_python/python.py,sha256=uoWCm_X2_yGaOdnxAb_h2Mr8RjLd5MjuVL4cPF15E2E,41973
13
+ csspin_python/python_sbom.py,sha256=AT_dFEKoghZTVkkTECz0cOo-w3AZAYAgROx4prhAHKw,13603
14
14
  csspin_python/python_sbom_schema.yaml,sha256=QysysjGJ6n1WEoLBqYHcdbojuEomUvL8QG4ZSWQW2_8,739
15
- csspin_python/python_schema.yaml,sha256=pgVVjByUYjxQWek7aFmjQzRwmq2ROLvHYgwGPMrT9sM,6351
15
+ csspin_python/python_schema.yaml,sha256=RwJgbyIRenj18-s60HmqkwWCGtd6G1acpJ9TDI9QjBQ,6448
16
16
  csspin_python/radon.py,sha256=uFqm6FEi5oWj-_XVaAm3s9cam0cUmr1_FwRf40K6xWs,1876
17
17
  csspin_python/radon_schema.yaml,sha256=rlRzXw5z4XbjOVznRiUxWGP4E9hx1Jm-gGw1iQiYzE0,548
18
- csspin_python/uv_provisioner.py,sha256=1e-_Sb39JrqNWyaUNeBX59R5tutXLJ1ZsT7urCN1U0I,6044
18
+ csspin_python/uv_provisioner.py,sha256=2M1I8PqWTCUxqaOeJI5rchmly6t-MnOPQfIx-kKzdQU,6757
19
19
  csspin_python/uv_provisioner_schema.yaml,sha256=Y8ZNC2OMnhR8Us3WUXAXK9hMjqGWAKFJB2puX4X5XNQ,727
20
- csspin_python-5.0.0.dist-info/licenses/LICENSE,sha256=4MAecetnRTQw5DlHtiikDSzKWO1xVLwzM5_DsPMYlnE,10172
21
- csspin_python-5.0.0.dist-info/METADATA,sha256=a4GiqeHydRsKdxlj_uH-B9bAfoCfbQBSowJJrnCMaJI,5254
22
- csspin_python-5.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
23
- csspin_python-5.0.0.dist-info/top_level.txt,sha256=QSeglMEGbFu1z4L6MCQYwo01NgL0KojWvC4rzgMQ8gU,14
24
- csspin_python-5.0.0.dist-info/RECORD,,
20
+ csspin_python-6.0.0.dist-info/licenses/LICENSE,sha256=4MAecetnRTQw5DlHtiikDSzKWO1xVLwzM5_DsPMYlnE,10172
21
+ csspin_python-6.0.0.dist-info/METADATA,sha256=a-NIDfoER1W6_3YNyaxS_RKPKLXmWp6tk-uApcS6Lr4,5254
22
+ csspin_python-6.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
23
+ csspin_python-6.0.0.dist-info/top_level.txt,sha256=QSeglMEGbFu1z4L6MCQYwo01NgL0KojWvC4rzgMQ8gU,14
24
+ csspin_python-6.0.0.dist-info/RECORD,,