csspin-python 2.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.
@@ -0,0 +1,195 @@
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)
@@ -0,0 +1,38 @@
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.
@@ -0,0 +1,150 @@
1
+ # -*- mode: python; coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2022 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 behave plugin for spin"""
20
+
21
+ import contextlib
22
+ import sys
23
+ from typing import Generator
24
+
25
+ from csspin import config, die, info, option, rmtree, setenv, sh, task, writetext
26
+ from csspin.tree import ConfigTree
27
+ from path import Path
28
+
29
+ defaults = config(
30
+ # Exclude the flaky tests in the defaults for now.
31
+ # Will switch the default back to True as soon as
32
+ # we have an easy way to set this in the CI.
33
+ flaky=False,
34
+ coverage=False,
35
+ cov_report="python-at-coverage.xml",
36
+ cov_config="setup.cfg",
37
+ # Default to concise and readable output
38
+ opts=[
39
+ "--no-source",
40
+ "--tags=~skip",
41
+ "--format=pretty",
42
+ "--no-skipped",
43
+ ],
44
+ report=config(
45
+ name="cept_test_results.json",
46
+ format="json.pretty",
47
+ ),
48
+ # This is the default location of behave tests
49
+ tests=["tests/accepttests"],
50
+ requires=config(
51
+ spin=[
52
+ "csspin_python.python",
53
+ ],
54
+ python=[
55
+ "behave",
56
+ "coverage",
57
+ ],
58
+ ),
59
+ )
60
+
61
+
62
+ def configure(cfg: ConfigTree) -> None:
63
+ """Add some runtime-dependent options"""
64
+ if sys.platform == "win32":
65
+ cfg.behave.opts.append("--tags=~linux")
66
+ else:
67
+ cfg.behave.opts.append("--tags=~windows")
68
+
69
+
70
+ def create_coverage_pth(cfg: ConfigTree) -> str: # pylint: disable=unused-argument
71
+ """Creating the coverage path file and returning its path"""
72
+ coverage_pth_path = cfg.python.site_packages / "coverage.pth"
73
+ info(f"Create {coverage_pth_path}")
74
+ writetext(coverage_pth_path, "import coverage; coverage.process_startup()")
75
+ return coverage_pth_path
76
+
77
+
78
+ @contextlib.contextmanager
79
+ def with_coverage(cfg: ConfigTree) -> Generator:
80
+ """Context-manager enabling to run coverage"""
81
+ coverage_pth = ""
82
+ try:
83
+
84
+ sh("coverage", "erase", check=False)
85
+ setenv(COVERAGE_PROCESS_START=cfg.behave.cov_config)
86
+ coverage_pth = create_coverage_pth(cfg)
87
+ yield
88
+ finally:
89
+ setenv(COVERAGE_PROCESS_START=None)
90
+ rmtree(coverage_pth)
91
+ sh("coverage", "combine", check=False)
92
+ sh("coverage", "report", check=False)
93
+ sh("coverage", "xml", "-o", cfg.behave.cov_report, check=False)
94
+
95
+
96
+ @task(when="cept")
97
+ def behave( # pylint: disable=too-many-arguments,too-many-positional-arguments
98
+ cfg,
99
+ instance: option(
100
+ "-i", # noqa: F821
101
+ "--instance", # noqa: F821
102
+ help="Directory of the CONTACT Elements instance.", # noqa: F722
103
+ ),
104
+ coverage: option(
105
+ "-c", # noqa: F821
106
+ "--coverage", # noqa: F821
107
+ is_flag=True,
108
+ help="Run the tests while collecting coverage.", # noqa: F722
109
+ ),
110
+ debug: option(
111
+ "--debug", is_flag=True, help="Start debug server." # noqa: F722,F821
112
+ ),
113
+ with_test_report: option(
114
+ "--with-test-report", # noqa: F722
115
+ is_flag=True,
116
+ help="Create a test execution report.", # noqa: F722
117
+ ),
118
+ args,
119
+ ):
120
+ """Run Gherkin tests using behave."""
121
+ # pylint: disable=missing-function-docstring
122
+ coverage_enabled = coverage or cfg.behave.coverage
123
+ coverage_context = with_coverage if coverage_enabled else contextlib.nullcontext
124
+ opts = cfg.behave.opts
125
+ if not cfg.behave.flaky:
126
+ opts.append("--tags=~flaky")
127
+ if with_test_report and cfg.behave.report.name and cfg.behave.report.format:
128
+ opts = [
129
+ f"--format={cfg.behave.report.format}",
130
+ f"-o={cfg.behave.report.name}",
131
+ ] + opts
132
+ if cfg.loaded.get("csspin_ce.mkinstance"):
133
+ inst = Path(instance or cfg.mkinstance.base.instance_location).absolute()
134
+ if not (inst).is_dir():
135
+ die(f"Cannot find the CE instance '{inst}'.")
136
+ setenv(CADDOK_BASE=inst)
137
+
138
+ cmd = ["powerscript"]
139
+ if debug:
140
+ cmd.append("--debugpy")
141
+
142
+ with coverage_context(cfg):
143
+ sh(*cmd, "-m", "behave", *opts, *args, *cfg.behave.tests)
144
+ else:
145
+ cmd = ["python"]
146
+ if debug:
147
+ cmd = ["debugpy"] + cfg.debugpy.opts
148
+
149
+ with coverage_context(cfg):
150
+ sh(*cmd, "-m", "behave", *opts, *args, *cfg.behave.tests)
@@ -0,0 +1,39 @@
1
+ # -*- mode: yaml; coding: utf-8 -*-
2
+ #
3
+ # Schema for the behave plugin for spin
4
+
5
+ behave:
6
+ type: object
7
+ help: |
8
+ The behave plugin for spin enables running behave tests in the
9
+ context of spin.
10
+ properties:
11
+ flaky:
12
+ type: bool
13
+ help: Enable or disable flaky tests
14
+ coverage:
15
+ type: bool
16
+ help: Collect coverage while running behave.
17
+ cov_report:
18
+ type: path
19
+ help: Filepath to store the coverage report.
20
+ cov_config:
21
+ type: path
22
+ help: Filepath to the configuration that behave should respect.
23
+ opts:
24
+ type: list
25
+ help: Additional options for the behave command.
26
+ tests:
27
+ type: list
28
+ help: List of test files or directories to include.
29
+ report:
30
+ type: object
31
+ help: Configuration regarding the test report generation.
32
+ properties:
33
+ name:
34
+ type: path
35
+ help: Path to write the test report to.
36
+ format:
37
+ type: str
38
+ help: |
39
+ A valid formatter of behave to use for the test report.
@@ -0,0 +1,32 @@
1
+ # -*- mode: python; coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2022 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 providing configurations for the debugpy plugin for spin"""
20
+
21
+ from csspin import config
22
+
23
+ defaults = config(
24
+ opts=[
25
+ "--listen localhost:5678",
26
+ "--wait-for-client",
27
+ ],
28
+ requires=config(
29
+ spin=["csspin_python.python"],
30
+ python=["debugpy"],
31
+ ),
32
+ )
@@ -0,0 +1,14 @@
1
+ # -*- mode: yaml; coding: utf-8 -*-
2
+ #
3
+ # Schema for the debugpy plugin for spin
4
+
5
+ debugpy:
6
+ type: object
7
+ help: Configuration of the debugpy plugin for spin
8
+ properties:
9
+ enabled:
10
+ type: bool
11
+ help: Enable debugpy for usage.
12
+ opts:
13
+ type: list
14
+ help: Additional options used when executing debugpy.
csspin_python/devpi.py ADDED
@@ -0,0 +1,82 @@
1
+ # -*- mode: python; coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2020 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 devpi plugin for spin"""
19
+
20
+
21
+ from csspin import Command, config, die, exists, readyaml, setenv, sh, task
22
+
23
+ defaults = config(
24
+ formats=["bdist_wheel"],
25
+ url=None,
26
+ user=None,
27
+ requires=config(
28
+ spin=["csspin_python.python"],
29
+ python=[
30
+ "devpi-client",
31
+ "keyring",
32
+ ],
33
+ ),
34
+ )
35
+
36
+
37
+ def init(cfg): # pylint: disable=unused-argument
38
+ """Sets some environment variables"""
39
+ setenv(DEVPI_VENV="{python.venv}", DEVPI_CLIENTDIR="{spin.spin_dir}/devpi")
40
+
41
+
42
+ @task("devpi:upload")
43
+ def upload(cfg):
44
+ """Upload project wheel to a package server."""
45
+ if not cfg.devpi.user:
46
+ die("devpi.user is required!")
47
+
48
+ if exists(current_json := f"{cfg.spin.spin_dir}/devpi/current.json"):
49
+ data = readyaml(current_json)
50
+ else:
51
+ data = {}
52
+
53
+ devpi_ = Command("devpi")
54
+
55
+ if data.get("index") != (url := cfg.devpi.url):
56
+ if url == "None":
57
+ die("devpi.url not provided!")
58
+ devpi_("use", "-t", "yes", url)
59
+
60
+ devpi_("login", cfg.devpi.user)
61
+ devpi_(
62
+ "upload",
63
+ "-p",
64
+ cfg.python.python,
65
+ "--no-vcs",
66
+ f"--wheel={','.join(cfg.devpi.formats)}",
67
+ )
68
+
69
+
70
+ @task()
71
+ def devpi(cfg, args):
72
+ """Run the 'devpi' command inside the project's virtual environment.
73
+
74
+ All command line arguments are simply passed through to 'devpi'.
75
+
76
+ """
77
+ if cfg.devpi.url:
78
+ sh("devpi", "use", cfg.devpi.url)
79
+ if cfg.devpi.user:
80
+ sh("devpi", "login", cfg.devpi.user)
81
+
82
+ sh("devpi", *args)
@@ -0,0 +1,17 @@
1
+ # -*- mode: yaml; coding: utf-8 -*-
2
+ #
3
+ # Schema for the devpi plugin for spin
4
+
5
+ devpi:
6
+ type: object
7
+ help: Configuration of the devpi plugin for spin
8
+ properties:
9
+ formats:
10
+ type: list
11
+ help: A list of formats to pass to devpi's --wheel option
12
+ url:
13
+ type: str
14
+ help: The URL of the package server to communicate with
15
+ user:
16
+ type: str
17
+ help: The user to authenticate with the package server