csspin-python 2.0.0__py3-none-any.whl → 2.1.1__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/behave.py CHANGED
@@ -20,7 +20,7 @@
20
20
 
21
21
  import contextlib
22
22
  import sys
23
- from typing import Generator
23
+ from typing import Generator, Iterable
24
24
 
25
25
  from csspin import config, die, info, option, rmtree, setenv, sh, task, writetext
26
26
  from csspin.tree import ConfigTree
@@ -67,16 +67,16 @@ def configure(cfg: ConfigTree) -> None:
67
67
  cfg.behave.opts.append("--tags=~windows")
68
68
 
69
69
 
70
- def create_coverage_pth(cfg: ConfigTree) -> str: # pylint: disable=unused-argument
70
+ def create_coverage_pth(cfg: ConfigTree) -> Path: # pylint: disable=unused-argument
71
71
  """Creating the coverage path file and returning its path"""
72
- coverage_pth_path = cfg.python.site_packages / "coverage.pth"
72
+ coverage_pth_path: Path = cfg.python.site_packages / "coverage.pth"
73
73
  info(f"Create {coverage_pth_path}")
74
74
  writetext(coverage_pth_path, "import coverage; coverage.process_startup()")
75
75
  return coverage_pth_path
76
76
 
77
77
 
78
78
  @contextlib.contextmanager
79
- def with_coverage(cfg: ConfigTree) -> Generator:
79
+ def with_coverage(cfg: ConfigTree) -> Generator[None, None, None]:
80
80
  """Context-manager enabling to run coverage"""
81
81
  coverage_pth = ""
82
82
  try:
@@ -95,28 +95,28 @@ def with_coverage(cfg: ConfigTree) -> Generator:
95
95
 
96
96
  @task(when="cept")
97
97
  def behave( # pylint: disable=too-many-arguments,too-many-positional-arguments
98
- cfg,
99
- instance: option(
98
+ cfg: ConfigTree,
99
+ instance: option( # type: ignore[valid-type]
100
100
  "-i", # noqa: F821
101
101
  "--instance", # noqa: F821
102
102
  help="Directory of the CONTACT Elements instance.", # noqa: F722
103
103
  ),
104
- coverage: option(
104
+ coverage: option( # type: ignore[valid-type]
105
105
  "-c", # noqa: F821
106
106
  "--coverage", # noqa: F821
107
107
  is_flag=True,
108
108
  help="Run the tests while collecting coverage.", # noqa: F722
109
109
  ),
110
- debug: option(
110
+ debug: option( # type: ignore[valid-type]
111
111
  "--debug", is_flag=True, help="Start debug server." # noqa: F722,F821
112
112
  ),
113
- with_test_report: option(
113
+ with_test_report: option( # type: ignore[valid-type]
114
114
  "--with-test-report", # noqa: F722
115
115
  is_flag=True,
116
116
  help="Create a test execution report.", # noqa: F722
117
117
  ),
118
- args,
119
- ):
118
+ args: Iterable[str],
119
+ ) -> None:
120
120
  """Run Gherkin tests using behave."""
121
121
  # pylint: disable=missing-function-docstring
122
122
  coverage_enabled = coverage or cfg.behave.coverage
csspin_python/devpi.py CHANGED
@@ -18,7 +18,10 @@
18
18
  """Module implementing the devpi plugin for spin"""
19
19
 
20
20
 
21
+ from typing import Iterable
22
+
21
23
  from csspin import Command, config, die, exists, readyaml, setenv, sh, task
24
+ from csspin.tree import ConfigTree
22
25
 
23
26
  defaults = config(
24
27
  formats=["bdist_wheel"],
@@ -34,13 +37,13 @@ defaults = config(
34
37
  )
35
38
 
36
39
 
37
- def init(cfg): # pylint: disable=unused-argument
40
+ def init(cfg: ConfigTree) -> None: # pylint: disable=unused-argument
38
41
  """Sets some environment variables"""
39
42
  setenv(DEVPI_VENV="{python.venv}", DEVPI_CLIENTDIR="{spin.spin_dir}/devpi")
40
43
 
41
44
 
42
45
  @task("devpi:upload")
43
- def upload(cfg):
46
+ def upload(cfg: ConfigTree) -> None:
44
47
  """Upload project wheel to a package server."""
45
48
  if not cfg.devpi.user:
46
49
  die("devpi.user is required!")
@@ -68,7 +71,7 @@ def upload(cfg):
68
71
 
69
72
 
70
73
  @task()
71
- def devpi(cfg, args):
74
+ def devpi(cfg: ConfigTree, args: Iterable[str]) -> None:
72
75
  """Run the 'devpi' command inside the project's virtual environment.
73
76
 
74
77
  All command line arguments are simply passed through to 'devpi'.
@@ -17,7 +17,10 @@
17
17
 
18
18
  """Module implementing the playwright plugin for spin"""
19
19
 
20
+ from typing import Iterable
21
+
20
22
  from csspin import Path, Verbosity, config, die, option, setenv, sh, task
23
+ from csspin.tree import ConfigTree
21
24
 
22
25
  defaults = config(
23
26
  browsers_path="{spin.data}/playwright_browsers",
@@ -50,29 +53,29 @@ defaults = config(
50
53
 
51
54
  @task(when="cept")
52
55
  def playwright( # pylint: disable=too-many-arguments,too-many-positional-arguments
53
- cfg,
54
- instance: option(
56
+ cfg: ConfigTree,
57
+ instance: option( # type: ignore[valid-type]
55
58
  "-i", # noqa: F821
56
59
  "--instance", # noqa: F821
57
60
  default=None,
58
61
  help="Directory of the CONTACT Elements instance.", # noqa: F722
59
62
  ),
60
- coverage: option(
63
+ coverage: option( # type: ignore[valid-type]
61
64
  "-c", # noqa: F821
62
65
  "--coverage", # noqa: F821
63
66
  is_flag=True,
64
67
  help="Run the tests while collecting coverage.", # noqa: F722
65
68
  ),
66
- debug: option(
69
+ debug: option( # type: ignore[valid-type]
67
70
  "--debug", is_flag=True, help="Start debug server." # noqa: F722,F821
68
71
  ),
69
- with_test_report: option(
72
+ with_test_report: option( # type: ignore[valid-type]
70
73
  "--with-test-report", # noqa: F722
71
74
  is_flag=True,
72
75
  help="Create a test execution report.", # noqa: F722
73
76
  ),
74
- args,
75
- ):
77
+ args: Iterable[str],
78
+ ) -> None:
76
79
  """Run the playwright tests with pytest."""
77
80
  setenv(
78
81
  PLAYWRIGHT_BROWSERS_PATH=cfg.playwright.browsers_path,
@@ -113,7 +116,7 @@ def playwright( # pylint: disable=too-many-arguments,too-many-positional-argume
113
116
  sh(*cmd, *opts, *args, *cfg.playwright.tests)
114
117
 
115
118
 
116
- def _download_playwright_browsers(cfg):
119
+ def _download_playwright_browsers(cfg: ConfigTree) -> None:
117
120
  """Let playwright install the browsers"""
118
121
  sh(
119
122
  f"playwright install {' '.join(cfg.playwright.browsers)}",
@@ -121,6 +124,6 @@ def _download_playwright_browsers(cfg):
121
124
  )
122
125
 
123
126
 
124
- def provision(cfg):
127
+ def provision(cfg: ConfigTree) -> None:
125
128
  """Install playwright browsers during provisioning"""
126
129
  _download_playwright_browsers(cfg)
csspin_python/pytest.py CHANGED
@@ -18,7 +18,10 @@
18
18
  """Module implementing the pytest plugin for spin"""
19
19
 
20
20
 
21
+ from typing import Iterable
22
+
21
23
  from csspin import Path, Verbosity, config, die, option, setenv, sh, task
24
+ from csspin.tree import ConfigTree
22
25
 
23
26
  defaults = config(
24
27
  coverage=False,
@@ -49,29 +52,29 @@ defaults = config(
49
52
 
50
53
  @task(when="test")
51
54
  def pytest( # pylint: disable=too-many-arguments,too-many-positional-arguments
52
- cfg,
53
- instance: option(
55
+ cfg: ConfigTree,
56
+ instance: option( # type: ignore[valid-type]
54
57
  "-i", # noqa: F821
55
58
  "--instance", # noqa: F821
56
59
  default=None,
57
60
  help="Directory of the CONTACT Elements instance.", # noqa: F722
58
61
  ),
59
- coverage: option(
62
+ coverage: option( # type: ignore[valid-type]
60
63
  "-c", # noqa: F821
61
64
  "--coverage", # noqa: F821
62
65
  is_flag=True,
63
66
  help="Run the tests while collecting coverage.", # noqa: F722
64
67
  ),
65
- debug: option(
68
+ debug: option( # type: ignore[valid-type]
66
69
  "--debug", is_flag=True, help="Start debug server." # noqa: F722,F821
67
70
  ),
68
- with_test_report: option(
71
+ with_test_report: option( # type: ignore[valid-type]
69
72
  "--with-test-report", # noqa: F722
70
73
  is_flag=True,
71
74
  help="Create a test execution report.", # noqa: F722
72
75
  ),
73
- args,
74
- ):
76
+ args: Iterable[str],
77
+ ) -> None:
75
78
  """Run the 'pytest' command."""
76
79
  opts = cfg.pytest.opts
77
80
  if cfg.verbosity == Verbosity.QUIET:
csspin_python/python.py CHANGED
@@ -15,7 +15,7 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
  #
18
- # pylint: disable=too-few-public-methods,missing-class-docstring
18
+ # pylint: disable=too-few-public-methods,missing-class-docstring,too-many-lines
19
19
 
20
20
  """``python``
21
21
  ==========
@@ -75,6 +75,14 @@ import shutil
75
75
  import sys
76
76
  from subprocess import check_output
77
77
  from textwrap import dedent, indent
78
+ from typing import Iterable, Type, Union
79
+
80
+ try:
81
+ from typing import Self # type: ignore[attr-defined]
82
+ except ImportError:
83
+ from typing import TypeVar
84
+
85
+ Self = TypeVar("Self") # type: ignore[misc]
78
86
 
79
87
  from click.exceptions import Abort
80
88
  from csspin import (
@@ -148,6 +156,13 @@ defaults = config(
148
156
  install=True,
149
157
  extras=[],
150
158
  ),
159
+ aws_auth=config(
160
+ enabled=False,
161
+ memo="{spin.spin_dir}/aws_auth.memo",
162
+ key_duration=3600 * 10, # 10 hours
163
+ static_oidc=False,
164
+ index="16.0/simple",
165
+ ),
151
166
  index_url="https://pypi.org/simple",
152
167
  requires=config(
153
168
  python=["build", "wheel"],
@@ -178,16 +193,16 @@ defaults = config(
178
193
 
179
194
 
180
195
  @task()
181
- def python(args):
196
+ def python(args: Iterable[object]) -> None:
182
197
  """Run the Python interpreter used for this projects."""
183
198
  sh("python", *args)
184
199
 
185
200
 
186
201
  @task("python:wheel", when="package")
187
202
  def wheel(
188
- cfg,
189
- paths: argument(type=str, nargs=-1, required=False), # noqa: F722
190
- ):
203
+ cfg: ConfigTree,
204
+ paths: argument(type=str, nargs=-1, required=False), # type: ignore[valid-type]
205
+ ) -> None:
191
206
  """Build a wheel of the current project and any additional wheels."""
192
207
  setenv(PIP_INDEX_URL=cfg.python.index_url)
193
208
  search_paths = paths or cfg.python.build_wheels
@@ -219,8 +234,13 @@ def wheel(
219
234
 
220
235
 
221
236
  @task()
222
- def env():
223
- """Generate command to activate the virtual environment"""
237
+ def env() -> None:
238
+ """
239
+ Generate command to activate the virtual environment
240
+
241
+ NOTE: spin itself should not be run from within the activated virtual
242
+ environment!
243
+ """
224
244
  if sys.platform == "win32":
225
245
  # Don't care about cmd
226
246
  print(normpath("{python.scriptdir}", "activate.ps1"))
@@ -228,12 +248,12 @@ def env():
228
248
  print(f". {normpath('{python.scriptdir}', 'activate')}")
229
249
 
230
250
 
231
- def pyenv_install(cfg):
251
+ def pyenv_install(cfg: ConfigTree) -> None:
232
252
  """Install and setup the virtual environment using pyenv"""
233
253
  with namespaces(cfg.python):
234
254
  if cfg.python.user_pyenv:
235
255
  info("Using your existing pyenv installation ...")
236
- sh(f"pyenv install --skip-existing {cfg.python.version}")
256
+ sh("pyenv", "install", "--skip-existing", {cfg.python.version})
237
257
  cfg.python.interpreter = backtick("pyenv which python --nosystem").strip()
238
258
  else:
239
259
  info("Installing Python {version} to {inst_dir}")
@@ -241,16 +261,18 @@ def pyenv_install(cfg):
241
261
  # pyenv is by far the most robust way to install a
242
262
  # version of Python.
243
263
  if not exists("{pyenv.path}"):
244
- sh(f"git clone {cfg.python.pyenv.url} {cfg.python.pyenv.path}")
264
+ sh("git", "clone", cfg.python.pyenv.url, cfg.python.pyenv.path)
245
265
  else:
246
266
  with cd(cfg.python.pyenv.path):
247
- sh("git pull")
267
+ sh("git", "pull")
248
268
  # we should set
249
269
  setenv(PYTHON_BUILD_CACHE_PATH=mkdir(cfg.python.pyenv.cache))
250
270
  setenv(PYTHON_CFLAGS="-DOPENSSL_NO_COMP")
251
271
  try:
252
272
  sh(
253
- f"{cfg.python.pyenv.python_build} {cfg.python.version} {cfg.python.inst_dir}"
273
+ cfg.python.pyenv.python_build,
274
+ cfg.python.version,
275
+ cfg.python.inst_dir,
254
276
  )
255
277
  except Abort:
256
278
  error("Failed to build the Python interpreter - removing it")
@@ -258,7 +280,7 @@ def pyenv_install(cfg):
258
280
  raise
259
281
 
260
282
 
261
- def nuget_install(cfg):
283
+ def nuget_install(cfg: ConfigTree) -> None:
262
284
  """Install the virtual environment using nuget"""
263
285
  if not exists(cfg.python.nuget.exe):
264
286
  download(cfg.python.nuget.url, cfg.python.nuget.exe)
@@ -276,7 +298,7 @@ def nuget_install(cfg):
276
298
  "-source",
277
299
  cfg.python.nuget.source,
278
300
  )
279
- sh(f"{cfg.python.interpreter} -m ensurepip --upgrade")
301
+ sh(cfg.python.interpreter, "-m", "ensurepip", "--upgrade")
280
302
  sh(
281
303
  cfg.python.interpreter,
282
304
  "-mpip",
@@ -307,7 +329,7 @@ def provision(cfg: ConfigTree) -> None:
307
329
  cfg.python.site_packages = get_site_packages(interpreter=cfg.python.python)
308
330
 
309
331
 
310
- def configure(cfg):
332
+ def configure(cfg: ConfigTree) -> None:
311
333
  """Configure the python plugin"""
312
334
  if not cfg.python.version and not cfg.python.use:
313
335
  die(
@@ -337,8 +359,11 @@ def configure(cfg):
337
359
  if exists(cfg.python.python):
338
360
  cfg.python.site_packages = get_site_packages(interpreter=cfg.python.python)
339
361
 
362
+ if cfg.python.aws_auth.enabled:
363
+ _check_aws_token_validity(cfg)
364
+
340
365
 
341
- def init(cfg):
366
+ def init(cfg: ConfigTree) -> None:
342
367
  """Initialize the python plugin"""
343
368
  if not cfg.python.use:
344
369
  logging.debug("Checking for %s", cfg.python.interpreter)
@@ -355,7 +380,7 @@ def init(cfg):
355
380
  ACTIVATED = False
356
381
 
357
382
 
358
- def venv_init(cfg):
383
+ def venv_init(cfg: ConfigTree) -> None:
359
384
  """Activate the virtual environment"""
360
385
  global ACTIVATED # pylint: disable=global-statement
361
386
  if os.environ.get("VIRTUAL_ENV", "") != cfg.python.venv and not ACTIVATED:
@@ -376,7 +401,38 @@ def venv_init(cfg):
376
401
  ACTIVATED = True
377
402
 
378
403
 
379
- def patch_activate(schema):
404
+ class ActivateScriptPatcher(abc.ABC):
405
+ activatescript: Union[str, Path]
406
+ setpattern: str
407
+ resetpattern: str
408
+ old_env_pattern: str
409
+ patchmarker: str
410
+ replacements: list[tuple[str, str]]
411
+ script: str
412
+
413
+ @staticmethod
414
+ @abc.abstractmethod
415
+ def interpolate_environ_value(value: str) -> str:
416
+ """
417
+ Translate value so the script can handle uninterpolated "{ENVVAR}" literals in value
418
+
419
+ Example:
420
+ # Assume the following subset of os.environ
421
+ os.environ = {
422
+ "PATH": "/bin:/usr/bin",
423
+ "COMPILER_PATHS": "/compiler/A/bin:/compiler/B/bin",
424
+ }
425
+
426
+ # Now, setenv has been called with
427
+ # setenv(PATH="{python.scriptdir}:{COMPILER_PATHS}:{PATH}") thus the
428
+ # value of ``PATH`` in ``EXPORTS`` equals "/venv/bin:{COMPILER_PATHS}:{PATH}" as
429
+ # ``COMPILER_PATHS`` and ``PATH`` haven't been interpolated yet.
430
+ interpolate_environ_value(value) => /venv/bin:/compiler/A/bin:/compiler/B/bin:/bin:/usr/bin
431
+ """
432
+ return value
433
+
434
+
435
+ def patch_activate(schema: Type[ActivateScriptPatcher]) -> None:
380
436
  """Patch the activate script"""
381
437
  if exists(schema.activatescript):
382
438
  setters = []
@@ -387,9 +443,9 @@ def patch_activate(schema):
387
443
  setters.append(schema.setpattern.format(name=name, value=value))
388
444
  resetters.add(schema.resetpattern.format(name=name))
389
445
  old_value_setters.add(schema.old_env_pattern.format(name=name))
390
- resetters = "\n".join(resetters)
391
- setters = "\n".join(setters)
392
- old_value_setters = "\n".join(old_value_setters)
446
+ resetters_string = "\n".join(resetters)
447
+ setters_string = "\n".join(setters)
448
+ old_value_setters_string = "\n".join(old_value_setters)
393
449
  original = readtext(schema.activatescript)
394
450
  if schema.patchmarker not in original:
395
451
  shutil.copyfile(
@@ -408,36 +464,13 @@ def patch_activate(schema):
408
464
  newscript = schema.script.format(
409
465
  patchmarker=schema.patchmarker,
410
466
  original=original,
411
- resetters=resetters,
412
- old_value_setters=old_value_setters,
413
- setters=setters,
467
+ resetters=resetters_string,
468
+ old_value_setters=old_value_setters_string,
469
+ setters=setters_string,
414
470
  )
415
471
  writetext(f"{schema.activatescript}", newscript)
416
472
 
417
473
 
418
- class ActivateScriptPatcher(abc.ABC):
419
- @staticmethod
420
- @abc.abstractmethod
421
- def interpolate_environ_value(value):
422
- """
423
- Translate value so the script can handle uninterpolated "{ENVVAR}" literals in value
424
-
425
- Example:
426
- # Assume the following subset of os.environ
427
- os.environ = {
428
- "PATH": "/bin:/usr/bin",
429
- "COMPILER_PATHS": "/compiler/A/bin:/compiler/B/bin",
430
- }
431
-
432
- # Now, setenv has been called with
433
- # setenv(PATH="{python.scriptdir}:{COMPILER_PATHS}:{PATH}") thus the
434
- # value of ``PATH`` in ``EXPORTS`` equals "/venv/bin:{COMPILER_PATHS}:{PATH}" as
435
- # ``COMPILER_PATHS`` and ``PATH`` haven't been interpolated yet.
436
- interpolate_environ_value(value) => /venv/bin:/compiler/A/bin:/compiler/B/bin:/bin:/usr/bin
437
- """
438
- return value
439
-
440
-
441
474
  class BashActivate(ActivateScriptPatcher):
442
475
  patchmarker = "\n## Patched by csspin_python.python\n"
443
476
  activatescript = Path("{python.scriptdir}") / "activate"
@@ -500,7 +533,7 @@ class BashActivate(ActivateScriptPatcher):
500
533
  )
501
534
 
502
535
  @staticmethod
503
- def interpolate_environ_value(value):
536
+ def interpolate_environ_value(value: str) -> str:
504
537
  if not value:
505
538
  return ""
506
539
  keys = re.findall(r"{(?P<key>\w+?)}", value)
@@ -554,7 +587,7 @@ class PowershellActivate(ActivateScriptPatcher):
554
587
  )
555
588
 
556
589
  @staticmethod
557
- def interpolate_environ_value(value):
590
+ def interpolate_environ_value(value: str) -> str:
558
591
  if not value:
559
592
  return ""
560
593
  keys = re.findall(r"{(?P<key>\w+?)}", value)
@@ -567,7 +600,7 @@ class PowershellActivate(ActivateScriptPatcher):
567
600
  class BatchActivate(ActivateScriptPatcher):
568
601
  patchmarker = "\nREM Patched by csspin_python.python\n"
569
602
  activatescript = Path("{python.scriptdir}") / "activate.bat"
570
- replacements = ()
603
+ replacements = []
571
604
  old_env_pattern = dedent(
572
605
  """
573
606
  if defined _OLD_SPIN_VALUE_{name} goto ENDIFSPIN{name}1
@@ -604,7 +637,7 @@ class BatchActivate(ActivateScriptPatcher):
604
637
  )
605
638
 
606
639
  @staticmethod
607
- def interpolate_environ_value(value):
640
+ def interpolate_environ_value(value: str) -> str:
608
641
  if not value:
609
642
  return ""
610
643
  keys = re.findall(r"{(?P<key>\w+?)}", value)
@@ -617,7 +650,7 @@ class BatchActivate(ActivateScriptPatcher):
617
650
  class BatchDeactivate(ActivateScriptPatcher):
618
651
  patchmarker = "\nREM Patched by csspin_python.python\n"
619
652
  activatescript = Path("{python.scriptdir}") / "deactivate.bat"
620
- replacements = ()
653
+ replacements = []
621
654
  old_env_pattern = ""
622
655
  setpattern = ""
623
656
  resetpattern = dedent(
@@ -648,7 +681,7 @@ class BatchDeactivate(ActivateScriptPatcher):
648
681
  class PythonActivate(ActivateScriptPatcher):
649
682
  patchmarker = "# Patched by csspin_python.python\n"
650
683
  activatescript = Path("{python.scriptdir}") / "activate_this.py"
651
- replacements = ()
684
+ replacements = []
652
685
  old_env_pattern = ""
653
686
  setpattern = 'os.environ["{name}"] = fr"{value}"'
654
687
  resetpattern = ""
@@ -661,7 +694,7 @@ class PythonActivate(ActivateScriptPatcher):
661
694
  )
662
695
 
663
696
  @staticmethod
664
- def interpolate_environ_value(value):
697
+ def interpolate_environ_value(value: str) -> str:
665
698
  if not value:
666
699
  return ""
667
700
  keys = re.findall(r"{(?P<key>\w+?)}", value)
@@ -671,7 +704,7 @@ class PythonActivate(ActivateScriptPatcher):
671
704
  return value
672
705
 
673
706
 
674
- def get_site_packages(interpreter):
707
+ def get_site_packages(interpreter: Path) -> Path:
675
708
  """Return the path to the virtual environments site-packages."""
676
709
  return Path(
677
710
  check_output(
@@ -686,7 +719,7 @@ def get_site_packages(interpreter):
686
719
  )
687
720
 
688
721
 
689
- def finalize_provision(cfg):
722
+ def finalize_provision(cfg: ConfigTree) -> None:
690
723
  """Patching the activate scripts and preparing the site-packages"""
691
724
  cfg.python.provisioner.install(cfg)
692
725
 
@@ -722,8 +755,11 @@ class ProvisionerProtocol:
722
755
  The provisioner will be memoized, so make sure it works with ``pickle.dumps``.
723
756
  """
724
757
 
758
+ requirements: set[str]
759
+ devpackages: set[str]
760
+
725
761
  # noinspection PyMethodMayBeStatic
726
- def provision_python(self, cfg: ConfigTree) -> None:
762
+ def provision_python(self: Self, cfg: ConfigTree) -> None:
727
763
  """Provision the project's python interpreter"""
728
764
  if sys.platform == "win32":
729
765
  nuget_install(cfg)
@@ -732,7 +768,7 @@ class ProvisionerProtocol:
732
768
  pyenv_install(cfg)
733
769
 
734
770
  # noinspection PyMethodMayBeStatic
735
- def provision_venv(self, cfg: ConfigTree) -> None:
771
+ def provision_venv(self: Self, cfg: ConfigTree) -> None:
736
772
  """Provision the virtual environment of the project"""
737
773
  # virtualenv is guaranteed to be available like this
738
774
  # as we declared it as one of spin's dependencies
@@ -751,26 +787,26 @@ class ProvisionerProtocol:
751
787
  env={"PYTHONPATH": cfg.spin.spin_dir / "plugins"},
752
788
  )
753
789
 
754
- def prerequisites(self, cfg: ConfigTree) -> None:
790
+ def prerequisites(self: Self, cfg: ConfigTree) -> None:
755
791
  """Provide requirements for the provisioning strategy."""
756
792
 
757
- def lock(self, cfg: ConfigTree) -> None:
793
+ def lock(self: Self, cfg: ConfigTree) -> None:
758
794
  """Lock the project's dependencies."""
759
795
 
760
- def add(self, cfg: ConfigTree, req: str, devpackage: bool = False) -> None:
796
+ def add(self: Self, cfg: ConfigTree, req: str, devpackage: bool = False) -> None:
761
797
  """Add an extra dependency (incl. development ones)."""
762
798
 
763
- def lock_extras(self, cfg: ConfigTree) -> None:
799
+ def lock_extras(self: Self, cfg: ConfigTree) -> None:
764
800
  """Lock the extra dependencies."""
765
801
 
766
- def sync(self, cfg: ConfigTree) -> None:
802
+ def sync(self: Self, cfg: ConfigTree) -> None:
767
803
  """Synchronize the environment with the locked dependencies."""
768
804
 
769
- def install(self, cfg: ConfigTree) -> None:
805
+ def install(self: Self, cfg: ConfigTree) -> None:
770
806
  """Install the project itself."""
771
807
 
772
808
  # noinspection PyMethodMayBeStatic
773
- def cleanup(self, cfg: ConfigTree) -> None:
809
+ def cleanup(self: Self, cfg: ConfigTree) -> None:
774
810
  """Cleanup the provisioned environment"""
775
811
  rmtree(cfg.python.venv)
776
812
 
@@ -782,12 +818,12 @@ class SimpleProvisioner(ProvisionerProtocol):
782
818
  longer required.
783
819
  """
784
820
 
785
- def __init__(self):
821
+ def __init__(self: Self) -> None:
786
822
  self.requirements = set()
787
823
  self.devpackages = set()
788
824
  self.m = Memoizer("{python.memo}")
789
825
 
790
- def prerequisites(self, cfg):
826
+ def prerequisites(self: Self, cfg: ConfigTree) -> None:
791
827
  # We'll need pip
792
828
  sh(
793
829
  "python",
@@ -801,23 +837,23 @@ class SimpleProvisioner(ProvisionerProtocol):
801
837
  "pip",
802
838
  )
803
839
 
804
- def lock(self, cfg):
840
+ def lock(self: Self, cfg: ConfigTree) -> None:
805
841
  """Noop"""
806
842
 
807
- def add(self, cfg, req, devpackage=False):
843
+ def add(self: Self, cfg: ConfigTree, req: str, devpackage: bool = False) -> None:
808
844
  # Add the requirement or devpackage if not already there.
809
845
  if not self.m.check(req):
810
846
  lst = self.devpackages if devpackage else self.requirements
811
847
  lst.add(req)
812
848
 
813
- def sync(self, cfg):
849
+ def sync(self: Self, cfg: ConfigTree) -> None:
814
850
  self.__execute_installation(
815
851
  self.requirements,
816
852
  None if cfg.verbosity > Verbosity.NORMAL else "-q",
817
853
  cfg.python.index_url,
818
854
  )
819
855
 
820
- def install(self, cfg):
856
+ def install(self: Self, cfg: ConfigTree) -> None:
821
857
  quietflag = None if cfg.verbosity > Verbosity.NORMAL else "-q"
822
858
  self.__execute_installation(self.devpackages, quietflag, cfg.python.index_url)
823
859
 
@@ -852,14 +888,16 @@ class SimpleProvisioner(ProvisionerProtocol):
852
888
  if pip_check.returncode:
853
889
  die(pip_check.stdout)
854
890
 
855
- def _split(self, reqset):
891
+ def _split(self: Self, reqset: set[str]) -> list[str]:
856
892
  """to pass whitespace-less args to sh()"""
857
893
  reqlist = []
858
894
  for req in reqset:
859
895
  reqlist.extend(req.split())
860
896
  return reqlist
861
897
 
862
- def __execute_installation(self, packages, quietflag, index_url):
898
+ def __execute_installation(
899
+ self: Self, packages: set[str], quietflag: Union[str, None], index_url: str
900
+ ) -> None:
863
901
  """Install packages that are not yet memoized"""
864
902
  if to_install := {package for package in packages if not self.m.check(package)}:
865
903
  sh(
@@ -876,7 +914,9 @@ class SimpleProvisioner(ProvisionerProtocol):
876
914
  self.m.save()
877
915
 
878
916
 
879
- def venv_provision(cfg): # pylint: disable=too-many-branches,missing-function-docstring
917
+ def venv_provision( # pylint: disable=too-many-branches,missing-function-docstring
918
+ cfg: ConfigTree,
919
+ ) -> None:
880
920
  fresh_env = False
881
921
 
882
922
  info("Checking venv '{python.venv}'")
@@ -888,13 +928,7 @@ def venv_provision(cfg): # pylint: disable=too-many-branches,missing-function-d
888
928
  # This sets PATH to the venv
889
929
  init(cfg)
890
930
 
891
- # Always create a pip conf with at least the packageserver index_url as content
892
- if sys.platform == "win32":
893
- pipconf = cfg.python.venv / "pip.ini"
894
- else:
895
- pipconf = cfg.python.venv / "pip.conf"
896
-
897
- _create_pipconf(cfg, pipconf)
931
+ _configure_pipconf(cfg)
898
932
 
899
933
  # Establish the prerequisites
900
934
  if fresh_env:
@@ -945,7 +979,9 @@ def cleanup(cfg: ConfigTree) -> None:
945
979
  f"'{provisioner.__class__.__name__}' failed: {err}"
946
980
  )
947
981
  memo.clear()
982
+
948
983
  rmtree(cfg.python.provisioner_memo)
984
+ rmtree(cfg.python.aws_auth.memo)
949
985
  for path in cfg.python.build_wheels:
950
986
  current_path = Path(interpolate1(path))
951
987
  rmtree(current_path / "build")
@@ -955,16 +991,89 @@ def cleanup(cfg: ConfigTree) -> None:
955
991
  rmtree(current_path / filename)
956
992
 
957
993
 
958
- def _create_pipconf(cfg, configfile):
959
- # pip cannot handle a section being defined twice, so let's insert the
960
- # index_url ourselves and create the configfile
994
+ def _get_pipconf(cfg: ConfigTree) -> Path:
995
+ """Retrieve the pipconf configuration file path."""
996
+ if sys.platform == "win32":
997
+ pipconf = interpolate1(Path(cfg.python.venv)) / "pip.ini"
998
+ else:
999
+ pipconf = interpolate1(Path(cfg.python.venv)) / "pip.conf"
1000
+
1001
+ return pipconf
1002
+
1003
+
1004
+ def _configure_pipconf(cfg: ConfigTree, update: bool = False) -> None:
1005
+ """Configure the pip configuration file"""
961
1006
  config_parser = configparser.ConfigParser()
962
1007
  config_parser.read_string(cfg.python.pipconf)
963
1008
  if not config_parser.has_section("global"):
964
1009
  config_parser.add_section("global")
965
- if not (
1010
+ if update or not (
966
1011
  "index_url" in config_parser["global"] or "index-url" in config_parser["global"]
967
1012
  ):
968
- config_parser["global"]["index_url"] = cfg.python.index_url
969
- with open(configfile, mode="w", encoding="utf-8") as fd:
1013
+ config_parser["global"]["index_url"] = interpolate1(cfg.python.index_url)
1014
+ with open(_get_pipconf(cfg), mode="w", encoding="utf-8") as fd:
970
1015
  config_parser.write(fd)
1016
+
1017
+
1018
+ def _obfuscate_index_url(index_url: str) -> None:
1019
+ """Add the CodeArtifact token to the secrets."""
1020
+
1021
+ from csspin import secrets
1022
+
1023
+ secrets.add(index_url.split(":")[2].split("@")[0]) # Codeartifact token
1024
+
1025
+
1026
+ def _check_aws_token_validity(cfg: ConfigTree) -> None:
1027
+ """
1028
+ If csspin-python[aws_auth] is installed, we can use csaccess to get the
1029
+ CodeArtifact authentication token.
1030
+ """
1031
+
1032
+ try:
1033
+ from csaccess import get_ca_pypi_url_programmatic
1034
+ except ImportError:
1035
+ die(
1036
+ "The 'aws_auth' feature requires the 'aws_auth' extra being"
1037
+ " installed (e.g. via csspin-python[aws_auth] in spinfile.yaml)."
1038
+ )
1039
+
1040
+ import time
1041
+
1042
+ current_time = int(time.time())
1043
+ timestamp_key = "aws_auth_timestamp"
1044
+
1045
+ with memoizer(cfg.python.aws_auth.memo) as memo:
1046
+ for item in memo.items():
1047
+ if isinstance(item, str) and item.startswith(f"{timestamp_key}:"):
1048
+ last_time = int(item.split(":", 1)[1])
1049
+ if current_time - last_time < cfg.python.aws_auth.key_duration:
1050
+ pipconf = _get_pipconf(cfg)
1051
+ config_parser = configparser.ConfigParser()
1052
+ config_parser.read(pipconf)
1053
+ info(f"Using existing index URL from {pipconf}.")
1054
+
1055
+ if index_url := (
1056
+ config_parser["global"].get("index_url")
1057
+ or config_parser["global"].get("index-url")
1058
+ ):
1059
+ cfg.python.index_url = index_url
1060
+ _obfuscate_index_url(index_url)
1061
+ break
1062
+ memo.items().remove(item)
1063
+ else:
1064
+ info("Updating Codeartifact token.")
1065
+ from urllib.parse import urljoin
1066
+
1067
+ index_url = urljoin(
1068
+ get_ca_pypi_url_programmatic(
1069
+ static_oidc=cfg.python.aws_auth.static_oidc
1070
+ )
1071
+ + "/",
1072
+ cfg.python.aws_auth.index,
1073
+ )
1074
+ cfg.python.index_url = index_url
1075
+ _obfuscate_index_url(index_url)
1076
+
1077
+ if exists(cfg.python.venv):
1078
+ _configure_pipconf(cfg, update=True)
1079
+ memo.add(f"{timestamp_key}:{current_time}")
@@ -129,3 +129,31 @@ python:
129
129
  help: |
130
130
  A list of packages that will be built along with the current
131
131
  project when executing python:wheel.
132
+ aws_auth:
133
+ type: object
134
+ help: Configuration for the 'aws_auth' extra.
135
+ properties:
136
+ enabled:
137
+ type: bool
138
+ help: |
139
+ Whether to enable AWS authentication for retrieving
140
+ packages from CodeArtifact.
141
+ memo:
142
+ type: path
143
+ help: |
144
+ Path to the memoized information regarding the aws_auth
145
+ extra.
146
+ key_duration:
147
+ type: int
148
+ help: |
149
+ Time in seconds defining how long the plugin should
150
+ consider the authentication token as valid before
151
+ issuing a new one.
152
+ static_oidc:
153
+ type: bool
154
+ help: |
155
+ Whether to static OIDC when authenticating with AWS
156
+ CodeArtifact.
157
+ index:
158
+ type: str
159
+ help: The Codeartifact repository index (e.g. "16.0/simple").
csspin_python/radon.py CHANGED
@@ -18,8 +18,10 @@
18
18
  """Module implementing the radon plugin for spin"""
19
19
 
20
20
  import logging
21
+ from typing import Iterable
21
22
 
22
23
  from csspin import config, info, option, sh, task
24
+ from csspin.tree import ConfigTree
23
25
 
24
26
  defaults = config(
25
27
  exe="radon",
@@ -36,20 +38,20 @@ defaults = config(
36
38
 
37
39
  @task()
38
40
  def radon(
39
- cfg,
40
- allsource: option(
41
+ cfg: ConfigTree,
42
+ allsource: option( # type: ignore[valid-type]
41
43
  "--all", # noqa: F821
42
44
  "allsource", # noqa: F821
43
45
  is_flag=True,
44
46
  help="Run for all src- and test-files.", # noqa: F722,F821
45
47
  ),
46
- args,
47
- ):
48
+ args: Iterable[str],
49
+ ) -> None:
48
50
  """Run radon to measure code complexity."""
49
51
  if allsource:
50
- files = ("{spin.project_root}/src", "{spin.project_root}/tests")
52
+ files = ["{spin.project_root}/src", "{spin.project_root}/tests"]
51
53
  else:
52
- files = args
54
+ files = list(args)
53
55
  if not files and hasattr(cfg, "vcs") and hasattr(cfg.vcs, "modified"):
54
56
  info("Found modified files.")
55
57
  files = cfg.vcs.modified
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
- Name: csspin_python
3
- Version: 2.0.0
2
+ Name: csspin-python
3
+ Version: 2.1.1
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>
@@ -20,21 +20,19 @@ Classifier: Topic :: Software Development
20
20
  Requires-Python: >=3.9
21
21
  Description-Content-Type: text/x-rst
22
22
  License-File: LICENSE
23
+ Requires-Dist: platformdirs~=4.3.8
23
24
  Requires-Dist: virtualenv
24
25
  Provides-Extra: aws-auth
25
- Requires-Dist: boto3; extra == "aws-auth"
26
- Requires-Dist: requests; extra == "aws-auth"
26
+ Requires-Dist: csaccess>=0.1.0; extra == "aws-auth"
27
27
  Dynamic: license-file
28
28
 
29
29
  |Latest Version| |Python| |License|
30
30
 
31
- `csspin_python` is maintained and published by `CONTACT Software GmbH`_ and
31
+ `csspin-python` is maintained and published by `CONTACT Software GmbH`_ and
32
32
  serves Python-based plugins for the `csspin`_ task runner.
33
33
 
34
34
  The following plugins are available:
35
35
 
36
- - `csspin_python.aws_auth`: A plugin for authenticating to CONTACT Elements
37
- instances hosted on AWS.
38
36
  - `csspin_python.behave`: A plugin for running tests using Behave.
39
37
  - `csspin_python.debugpy`: A plugin for debugging Python code using `debugpy`_.
40
38
  - `csspin_python.devpi`: A plugin for simplified usage of `devpi`_.
@@ -46,6 +44,9 @@ The following plugins are available:
46
44
  complexity.
47
45
  - `csspin_python.sphinx`: A plugin for building Sphinx documentation.
48
46
 
47
+ The package provides an ``aws_auth`` extra, that, if enabled, can
48
+ authenticate to `CONTACT Software GmbH`_'s AWS Codeartifact.
49
+
49
50
  Prerequisites
50
51
  -------------
51
52
 
@@ -56,10 +57,10 @@ Python package manager, e.g.:
56
57
 
57
58
  python -m pip install csspin
58
59
 
59
- Using csspin_python
60
+ Using csspin-python
60
61
  -------------------
61
62
 
62
- The `csspin_python` package and its plugins can be installed by defining those
63
+ The `csspin-python` package and its plugins can be installed by defining those
63
64
  within the `spinfile.yaml` configuration file of your project.
64
65
 
65
66
  .. code-block:: yaml
@@ -70,7 +71,7 @@ within the `spinfile.yaml` configuration file of your project.
70
71
  # To develop plugins comfortably, install the packages editable as
71
72
  # follows and add the relevant plugins to the list 'plugins' below
72
73
  plugin_packages:
73
- - csspin_python
74
+ - csspin-python
74
75
 
75
76
  # The list of plugins to be used for this project.
76
77
  plugins:
@@ -93,13 +94,13 @@ environment and install the required dependencies. After that, you can run
93
94
  tests using ``spin pytest`` and do other great things.
94
95
 
95
96
  .. _`CONTACT Software GmbH`: https://contact-software.com
96
- .. |Python| image:: https://img.shields.io/pypi/pyversions/csspin_python.svg?style=flat
97
- :target: https://pypi.python.org/pypi/csspin_python/
97
+ .. |Python| image:: https://img.shields.io/pypi/pyversions/csspin-python.svg?style=flat
98
+ :target: https://pypi.python.org/pypi/csspin-python/
98
99
  :alt: Supported Python Versions
99
- .. |Latest Version| image:: http://img.shields.io/pypi/v/csspin_python.svg?style=flat
100
+ .. |Latest Version| image:: http://img.shields.io/pypi/v/csspin-python.svg?style=flat
100
101
  :target: https://pypi.python.org/pypi/csspin/
101
102
  :alt: Latest Package Version
102
- .. |License| image:: http://img.shields.io/pypi/l/csspin_python.svg?style=flat
103
+ .. |License| image:: http://img.shields.io/pypi/l/csspin-python.svg?style=flat
103
104
  :target: https://www.apache.org/licenses/LICENSE-2.0.txt
104
105
  :alt: License
105
106
  .. _`csspin`: https://pypi.org/project/csspin
@@ -0,0 +1,19 @@
1
+ csspin_python/behave.py,sha256=iJZeyIqB7V_NzTdLTZldNY9W_GGwCWkXe6WY69wpDqs,4997
2
+ csspin_python/behave_schema.yaml,sha256=8qoOCK-uTmwgRRW29urgK0X_kgn0zO0X34v89bvii2w,1241
3
+ csspin_python/debugpy.py,sha256=v0ZZopv5TNoSaFf2kiePsw9OmhBpjfOBFh0u71jTcnQ,962
4
+ csspin_python/debugpy_schema.yaml,sha256=BeH30nSirDYctkdhS9xMXUG5htj3PED_ZjmxPG5WRUc,364
5
+ csspin_python/devpi.py,sha256=C-5O_vA06CwQR4uElOw-2VH2-m001SpxowM_X6RbRwo,2352
6
+ csspin_python/devpi_schema.yaml,sha256=2gPATWjVcfvCTrGZX2FK6wH8hh9KS0XzZ35JvZeJGEU,487
7
+ csspin_python/playwright.py,sha256=mzSsLcmewDZnZwdSyp5HytEMnXgkoJ9s1XXkd05eOwU,4254
8
+ csspin_python/playwright_schema.yaml,sha256=WFMok7dB7G6L8f8y_2_RKHjGe4ww1iUUS4tqCoUI1FE,1054
9
+ csspin_python/pytest.py,sha256=Mx6l4Cb28FjdZgL9Vd1zBbhcnYyc4QsqwkozksoKZJc,3189
10
+ csspin_python/pytest_schema.yaml,sha256=1bF8hNsJfV-LHUwGBBJ3GnQOZJiIQkG81DCBma2MalU,809
11
+ csspin_python/python.py,sha256=AbIX3r07c4tVAj0jIfjiRi7Vk9XFiap6AvS4xCYee48,34461
12
+ csspin_python/python_schema.yaml,sha256=F_PMK8D3KBvXK945b6-oRDoaxuDgxkBGqVPAJ-eFmv0,5970
13
+ csspin_python/radon.py,sha256=uFqm6FEi5oWj-_XVaAm3s9cam0cUmr1_FwRf40K6xWs,1876
14
+ csspin_python/radon_schema.yaml,sha256=rlRzXw5z4XbjOVznRiUxWGP4E9hx1Jm-gGw1iQiYzE0,548
15
+ csspin_python-2.1.1.dist-info/licenses/LICENSE,sha256=4MAecetnRTQw5DlHtiikDSzKWO1xVLwzM5_DsPMYlnE,10172
16
+ csspin_python-2.1.1.dist-info/METADATA,sha256=pIuulk5YG9JUQqORop0oV4GoX0DdHZoOOGktz8YkhYA,4209
17
+ csspin_python-2.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
+ csspin_python-2.1.1.dist-info/top_level.txt,sha256=QSeglMEGbFu1z4L6MCQYwo01NgL0KojWvC4rzgMQ8gU,14
19
+ csspin_python-2.1.1.dist-info/RECORD,,
csspin_python/aws_auth.py DELETED
@@ -1,195 +0,0 @@
1
- # -*- mode: python; coding: utf-8 -*-
2
- #
3
- # Copyright (C) 2025 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
-
19
- """Module implementing the aws_auth plugin for spin"""
20
-
21
- import configparser
22
- import os
23
-
24
- from csspin import Path, config, debug, die, exists, info, interpolate1
25
-
26
- defaults = config(
27
- aws_role_arn="arn:aws:iam::373369985286:role/cs-central1-codeartifact-ecr-read-role",
28
- aws_region="eu-central-1",
29
- aws_role_session_name="CodeArtifactSession",
30
- aws_codeartifact_domain="contact",
31
- aws_key_duration=3600,
32
- keycloak_url="https://login.contact-cloud.com/realms/contact/protocol/openid-connect/token",
33
- client_id="central1-auth-oidc-read",
34
- requires=config(
35
- spin=[
36
- "csspin_python.python",
37
- ],
38
- ),
39
- )
40
-
41
-
42
- def configure(cfg): # pylint: disable=too-many-statements
43
- """Configure the plugin and apply changes to the configuration tree"""
44
- # Could be useful in CI e.g. when you want to build docs
45
- # and need to include this plugin in spinfile
46
- # without using it's functionality
47
- if os.environ.get("AWS_AUTH_DISABLE"):
48
- info("AWS_AUTH_DISABLE is set, ignoring aws_auth plugin")
49
- return
50
-
51
- from sys import platform
52
-
53
- try:
54
- import boto3
55
- import requests
56
- from botocore.exceptions import ClientError
57
- except ImportError:
58
- die(
59
- "Failed to import required modules. Please install them by setting:"
60
- "\n\tplugin_packages:\n\t\t- csspin_python[aws_auth]\n"
61
- "in your project's spinfile.yaml"
62
- )
63
-
64
- cfg.aws_auth.client_secret = os.environ.get("KEYCLOAK_CLIENT_SECRET")
65
- if not cfg.aws_auth.client_secret:
66
- die(
67
- "Neither aws_auth.client_secret config"
68
- "entry nor KEYCLOAK_CLIENT_SECRET environment variable was found."
69
- )
70
-
71
- def get_keycloak_access_token(keycloak_url, client_id, client_secret):
72
- """
73
- Obtain the Keycloak access token using client credentials.
74
- """
75
- debug("Requesting Keycloak access token...")
76
- payload = {
77
- "grant_type": "client_credentials",
78
- "client_id": client_id,
79
- "client_secret": client_secret,
80
- }
81
-
82
- try:
83
- response = requests.post(keycloak_url, data=payload, timeout=15)
84
- response.raise_for_status()
85
- data = response.json()
86
- access_token = data.get("access_token")
87
- if not access_token:
88
- raise ValueError("Response doesn't contain access_token")
89
- except (ValueError, requests.exceptions.RequestException) as e:
90
- die(f"Failed to fetch Keycloak access token: {e}")
91
-
92
- return access_token
93
-
94
- def assume_aws_role_with_web_identity(
95
- keycloak_access_token,
96
- role_arn,
97
- role_session_name,
98
- region,
99
- key_duration_seconds,
100
- ):
101
- """
102
- Request AWS STS credentials using the Keycloak token as a web identity.
103
- """
104
- debug("Requesting AWS STS credentials...")
105
- sts_client = boto3.client("sts", region_name=region)
106
- try:
107
- sts_response = sts_client.assume_role_with_web_identity(
108
- RoleArn=role_arn,
109
- RoleSessionName=role_session_name,
110
- WebIdentityToken=keycloak_access_token,
111
- DurationSeconds=key_duration_seconds,
112
- )
113
- credentials = sts_response.get("Credentials", {})
114
- if not (
115
- credentials.get("AccessKeyId")
116
- and credentials.get("SecretAccessKey")
117
- and credentials.get("SessionToken")
118
- ):
119
- raise ValueError("Incomplete AWS credentials received")
120
- except (ValueError, ClientError) as e:
121
- die(f"Failed to assume AWS role with web identity: {e}")
122
-
123
- return credentials
124
-
125
- def get_codeartifact_auth_token(credentials, domain, region):
126
- """
127
- Retrieve the AWS CodeArtifact authentication token using temporary AWS credentials.
128
- """
129
- debug("Requesting CodeArtifact authentication token...")
130
- codeartifact_client = boto3.client(
131
- "codeartifact",
132
- region_name=region,
133
- aws_access_key_id=credentials.get("AccessKeyId"),
134
- aws_secret_access_key=credentials.get("SecretAccessKey"),
135
- aws_session_token=credentials.get("SessionToken"),
136
- )
137
-
138
- try:
139
- response = codeartifact_client.get_authorization_token(domain=domain)
140
- auth_token = response.get("authorizationToken")
141
- if not auth_token:
142
- raise ValueError("Failed to retrieve CodeArtifact authentication token")
143
- except (ValueError, ClientError) as e:
144
- die(f"Failed to retrieve CodeArtifact authentication token: {e}")
145
-
146
- return auth_token
147
-
148
- keycloak_access_token = get_keycloak_access_token(
149
- cfg.aws_auth.keycloak_url, cfg.aws_auth.client_id, cfg.aws_auth.client_secret
150
- )
151
-
152
- credentials = assume_aws_role_with_web_identity(
153
- keycloak_access_token,
154
- cfg.aws_auth.aws_role_arn,
155
- cfg.aws_auth.aws_role_session_name,
156
- cfg.aws_auth.aws_region,
157
- cfg.aws_auth.aws_key_duration,
158
- )
159
-
160
- codeartifact_auth_token = get_codeartifact_auth_token(
161
- credentials, cfg.aws_auth.aws_codeartifact_domain, cfg.aws_auth.aws_region
162
- )
163
-
164
- domain_owner = cfg.aws_auth.aws_role_arn.split(":")[4]
165
-
166
- cfg.aws_auth.codeartifact_auth_token = codeartifact_auth_token
167
- cfg.python.index_url = (
168
- f"https://aws:{codeartifact_auth_token}@"
169
- f"{cfg.aws_auth.aws_codeartifact_domain}-{domain_owner}"
170
- f".d.codeartifact.{cfg.aws_auth.aws_region}.amazonaws.com/pypi/elements/simple/"
171
- )
172
-
173
- pipconf = interpolate1(cfg.python.venv) / Path(
174
- "pip.ini" if platform == "win32" else "pip.conf"
175
- )
176
- if exists(pipconf):
177
- # Need to update pip.conf with the new index_url
178
- # for "spin run pip ..." to use the right index and
179
- # not the default one
180
- _update_pipconf_url(pipconf, cfg.python.index_url)
181
-
182
-
183
- def _update_pipconf_url(filename, url):
184
- """Upates the python.index_url in the pip.conf file with the new value"""
185
- info(f"Updating python.index_url in {filename} with a fresh token...")
186
- config_parser = configparser.ConfigParser()
187
- config_parser.read(filename)
188
- if not config_parser.has_section("global"):
189
- config_parser.add_section("global")
190
- option = (
191
- "index-url" if config_parser.has_option("global", "index-url") else "index_url"
192
- )
193
- config_parser.set("global", option, url)
194
- with open(filename, mode="w", encoding="utf-8") as f:
195
- config_parser.write(f)
@@ -1,38 +0,0 @@
1
- # -*- mode: yaml; coding: utf-8 -*-
2
- #
3
- # Schema of the aws_auth plugin for spin
4
-
5
- aws_auth:
6
- type: object
7
- help: |
8
- Configuration for the aws_auth plugin in spin.
9
- This plugin handles authentication with AWS and retrieves
10
- secret keys from AWS CodeArtifact.
11
- properties:
12
- aws_role_arn:
13
- type: str
14
- help: The ARN of the AWS IAM role to assume for authentication.
15
- aws_region:
16
- type: str
17
- help: The AWS region where the CodeArtifact repository is located.
18
- role_session_name:
19
- type: str
20
- help: The name for the AWS STS session when assuming a role.
21
- aws_codeartifact_domain:
22
- type: str
23
- help: The domain name of the AWS CodeArtifact repository.
24
- aws_key_duration:
25
- type: int
26
- help: The duration in seconds for which the temporary key will be valid.
27
- keycloak_url:
28
- type: str
29
- help: The URL of the Keycloak authentication server.
30
- client_id:
31
- type: str
32
- help: The client ID for authentication with Keycloak.
33
- client_secret:
34
- type: str
35
- help: The client secret for authentication with Keycloak.
36
- codeartifact_auth_token:
37
- type: str
38
- help: The CodeArtifact auth token.
@@ -1,21 +0,0 @@
1
- csspin_python/aws_auth.py,sha256=cQXz2AWW-TM-KAv8ZpXOSntTRRzvRL21f2NwMpOf3PA,7232
2
- csspin_python/aws_auth_schema.yaml,sha256=GgLhnwn4savhmLUcfL2eVPyNhWkiKA5tKz6y1tU1C6w,1353
3
- csspin_python/behave.py,sha256=iA5t-vwMDhmWoo-FHaeZ6JyIzj6k4r2WLyYa2AjXyp4,4815
4
- csspin_python/behave_schema.yaml,sha256=8qoOCK-uTmwgRRW29urgK0X_kgn0zO0X34v89bvii2w,1241
5
- csspin_python/debugpy.py,sha256=v0ZZopv5TNoSaFf2kiePsw9OmhBpjfOBFh0u71jTcnQ,962
6
- csspin_python/debugpy_schema.yaml,sha256=BeH30nSirDYctkdhS9xMXUG5htj3PED_ZjmxPG5WRUc,364
7
- csspin_python/devpi.py,sha256=2NhGYzq4aVzDa_d80uItUlYM9cA0uRFQ4rvnCryxL2k,2213
8
- csspin_python/devpi_schema.yaml,sha256=2gPATWjVcfvCTrGZX2FK6wH8hh9KS0XzZ35JvZeJGEU,487
9
- csspin_python/playwright.py,sha256=nYuPjuTfGx_6QPYsUpr-eAZARyXX_hZF8D4AxeI2u1s,4003
10
- csspin_python/playwright_schema.yaml,sha256=WFMok7dB7G6L8f8y_2_RKHjGe4ww1iUUS4tqCoUI1FE,1054
11
- csspin_python/pytest.py,sha256=EwxPynyPWS72NTtIah1jUGVa7fJYY8I2_NQz0y0U7Os,2978
12
- csspin_python/pytest_schema.yaml,sha256=1bF8hNsJfV-LHUwGBBJ3GnQOZJiIQkG81DCBma2MalU,809
13
- csspin_python/python.py,sha256=lWFSBeeqq3wM_DkR_zv3v9QlagXSZ-UMALhzxoAVXlY,30553
14
- csspin_python/python_schema.yaml,sha256=lhqZmepoGdWlBd27Bu3ErUDNtMLPM9oQ_fh71cdc4a8,4823
15
- csspin_python/radon.py,sha256=OSV0vTz7SMMHC82jUvWsBq6vtWolOjdiZ2bBdxplVJ4,1744
16
- csspin_python/radon_schema.yaml,sha256=rlRzXw5z4XbjOVznRiUxWGP4E9hx1Jm-gGw1iQiYzE0,548
17
- csspin_python-2.0.0.dist-info/licenses/LICENSE,sha256=4MAecetnRTQw5DlHtiikDSzKWO1xVLwzM5_DsPMYlnE,10172
18
- csspin_python-2.0.0.dist-info/METADATA,sha256=7I3BD1CdoU9z-1q4qvec-XFeB7UL_q1GLAe3-meg1so,4184
19
- csspin_python-2.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- csspin_python-2.0.0.dist-info/top_level.txt,sha256=QSeglMEGbFu1z4L6MCQYwo01NgL0KojWvC4rzgMQ8gU,14
21
- csspin_python-2.0.0.dist-info/RECORD,,