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 +11 -11
- csspin_python/devpi.py +6 -3
- csspin_python/playwright.py +12 -9
- csspin_python/pytest.py +10 -7
- csspin_python/python.py +196 -87
- csspin_python/python_schema.yaml +28 -0
- csspin_python/radon.py +8 -6
- {csspin_python-2.0.0.dist-info → csspin_python-2.1.1.dist-info}/METADATA +15 -14
- csspin_python-2.1.1.dist-info/RECORD +19 -0
- csspin_python/aws_auth.py +0 -195
- csspin_python/aws_auth_schema.yaml +0 -38
- csspin_python-2.0.0.dist-info/RECORD +0 -21
- {csspin_python-2.0.0.dist-info → csspin_python-2.1.1.dist-info}/WHEEL +0 -0
- {csspin_python-2.0.0.dist-info → csspin_python-2.1.1.dist-info}/licenses/LICENSE +0 -0
- {csspin_python-2.0.0.dist-info → csspin_python-2.1.1.dist-info}/top_level.txt +0 -0
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) ->
|
|
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'.
|
csspin_python/playwright.py
CHANGED
|
@@ -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), #
|
|
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
|
-
"""
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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=
|
|
412
|
-
old_value_setters=
|
|
413
|
-
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
959
|
-
|
|
960
|
-
|
|
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(
|
|
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}")
|
csspin_python/python_schema.yaml
CHANGED
|
@@ -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 =
|
|
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:
|
|
3
|
-
Version: 2.
|
|
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:
|
|
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
|
-
`
|
|
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
|
|
60
|
+
Using csspin-python
|
|
60
61
|
-------------------
|
|
61
62
|
|
|
62
|
-
The `
|
|
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
|
-
-
|
|
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/
|
|
97
|
-
:target: https://pypi.python.org/pypi/
|
|
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/
|
|
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/
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|