csspin-python 2.1.1__py3-none-any.whl → 3.0.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/playwright.py +23 -2
- csspin_python/playwright_schema.yaml +3 -1
- csspin_python/pytest.py +38 -1
- csspin_python/pytest_schema.yaml +19 -0
- csspin_python/python.py +121 -119
- csspin_python/python_schema.yaml +8 -15
- csspin_python/uv_provisioner.py +187 -0
- csspin_python/uv_provisioner_schema.yaml +24 -0
- {csspin_python-2.1.1.dist-info → csspin_python-3.0.1.dist-info}/METADATA +13 -4
- csspin_python-3.0.1.dist-info/RECORD +21 -0
- csspin_python-2.1.1.dist-info/RECORD +0 -19
- {csspin_python-2.1.1.dist-info → csspin_python-3.0.1.dist-info}/WHEEL +0 -0
- {csspin_python-2.1.1.dist-info → csspin_python-3.0.1.dist-info}/licenses/LICENSE +0 -0
- {csspin_python-2.1.1.dist-info → csspin_python-3.0.1.dist-info}/top_level.txt +0 -0
csspin_python/playwright.py
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
from typing import Iterable
|
|
21
21
|
|
|
22
|
-
from csspin import Path, Verbosity, config, die, option, setenv, sh, task
|
|
22
|
+
from csspin import Path, Verbosity, config, die, option, setenv, sh, task, warn
|
|
23
23
|
from csspin.tree import ConfigTree
|
|
24
24
|
|
|
25
25
|
defaults = config(
|
|
@@ -77,6 +77,16 @@ def playwright( # pylint: disable=too-many-arguments,too-many-positional-argume
|
|
|
77
77
|
args: Iterable[str],
|
|
78
78
|
) -> None:
|
|
79
79
|
"""Run the playwright tests with pytest."""
|
|
80
|
+
if cfg.pytest.playwright.enabled:
|
|
81
|
+
# This prevents the playwright tests from being run twice.
|
|
82
|
+
warn(
|
|
83
|
+
(
|
|
84
|
+
"The 'playwright' task has been skipped, as the playwright tests"
|
|
85
|
+
" are already being run by the csspin_python.pytest plugin."
|
|
86
|
+
" Please stop using the csspin_python.playwright plugin."
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
return
|
|
80
90
|
setenv(
|
|
81
91
|
PLAYWRIGHT_BROWSERS_PATH=cfg.playwright.browsers_path,
|
|
82
92
|
PACKAGE_NAME=cfg.spin.project_name,
|
|
@@ -124,6 +134,17 @@ def _download_playwright_browsers(cfg: ConfigTree) -> None:
|
|
|
124
134
|
)
|
|
125
135
|
|
|
126
136
|
|
|
127
|
-
def
|
|
137
|
+
def finalize_provision(cfg: ConfigTree) -> None:
|
|
128
138
|
"""Install playwright browsers during provisioning"""
|
|
129
139
|
_download_playwright_browsers(cfg)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def init(cfg: ConfigTree) -> None: # pylint: disable=unused-argument
|
|
143
|
+
"""Show deprecation notice in every spin call"""
|
|
144
|
+
warn(
|
|
145
|
+
(
|
|
146
|
+
"The csspin_python.playwright plugin will be removed with the next major release."
|
|
147
|
+
" Please use csspin_python.pytest with the 'pytest.playwright.enabled=True' setting"
|
|
148
|
+
" instead and stop using the csspin_python.playwright plugin."
|
|
149
|
+
)
|
|
150
|
+
)
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
playwright:
|
|
6
6
|
type: object
|
|
7
7
|
help: |
|
|
8
|
-
The pytest plugin provides the full pytest experience for spin.
|
|
8
|
+
The pytest plugin provides the full pytest experience for spin. This
|
|
9
|
+
plugin is deprecated, use the pytest plugin with the
|
|
10
|
+
'pytest.playwright.enabled=true' setting instead.
|
|
9
11
|
properties:
|
|
10
12
|
browsers_path:
|
|
11
13
|
type: path
|
csspin_python/pytest.py
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
from typing import Iterable
|
|
22
22
|
|
|
23
|
-
from csspin import Path, Verbosity, config, die, option, setenv, sh, task
|
|
23
|
+
from csspin import Path, Verbosity, config, die, interpolate1, option, setenv, sh, task
|
|
24
24
|
from csspin.tree import ConfigTree
|
|
25
25
|
|
|
26
26
|
defaults = config(
|
|
@@ -36,6 +36,11 @@ defaults = config(
|
|
|
36
36
|
opts=[],
|
|
37
37
|
tests=["cs", "tests"], # Strong convention @CONTACT
|
|
38
38
|
test_report="pytest.xml",
|
|
39
|
+
playwright=config(
|
|
40
|
+
enabled=False,
|
|
41
|
+
browsers_path="{spin.data}/playwright_browsers",
|
|
42
|
+
browsers=["chromium"],
|
|
43
|
+
),
|
|
39
44
|
requires=config(
|
|
40
45
|
spin=[
|
|
41
46
|
"csspin_python.debugpy",
|
|
@@ -50,6 +55,24 @@ defaults = config(
|
|
|
50
55
|
)
|
|
51
56
|
|
|
52
57
|
|
|
58
|
+
def _install_playwright_browsers(cfg: ConfigTree) -> None:
|
|
59
|
+
"""Let playwright install the browsers"""
|
|
60
|
+
sh(
|
|
61
|
+
f"playwright install {' '.join(cfg.pytest.playwright.browsers)}",
|
|
62
|
+
env={"PLAYWRIGHT_BROWSERS_PATH": cfg.pytest.playwright.browsers_path},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def configure(cfg: ConfigTree) -> None:
|
|
67
|
+
if interpolate1(cfg.pytest.playwright.enabled).lower() == "true":
|
|
68
|
+
cfg.pytest.requires.python.extend(["pytest-base-url", "pytest-playwright"])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def finalize_provision(cfg: ConfigTree) -> None:
|
|
72
|
+
if cfg.pytest.playwright.enabled:
|
|
73
|
+
_install_playwright_browsers(cfg)
|
|
74
|
+
|
|
75
|
+
|
|
53
76
|
@task(when="test")
|
|
54
77
|
def pytest( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
55
78
|
cfg: ConfigTree,
|
|
@@ -88,6 +111,20 @@ def pytest( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
|
88
111
|
else:
|
|
89
112
|
cmd = ["pytest"]
|
|
90
113
|
|
|
114
|
+
if cfg.pytest.playwright.enabled:
|
|
115
|
+
setenv(
|
|
116
|
+
PLAYWRIGHT_BROWSERS_PATH=cfg.pytest.playwright.browsers_path,
|
|
117
|
+
PACKAGE_NAME=cfg.spin.project_name,
|
|
118
|
+
)
|
|
119
|
+
for browser in cfg.pytest.playwright.browsers:
|
|
120
|
+
opts.extend(["--browser", browser])
|
|
121
|
+
# Run the browser download again, so that changes for
|
|
122
|
+
# cfg.pytest.playwright.browsers don't require a new provision call. If the
|
|
123
|
+
# browsers are already present it's more or less a noop.
|
|
124
|
+
_install_playwright_browsers(cfg)
|
|
125
|
+
if coverage or cfg.pytest.coverage:
|
|
126
|
+
setenv(PLAYWRIGHT_COVERAGE=1)
|
|
127
|
+
|
|
91
128
|
if cfg.loaded.get("csspin_ce.mkinstance"):
|
|
92
129
|
if not (
|
|
93
130
|
inst := Path(instance or cfg.mkinstance.base.instance_location).absolute()
|
csspin_python/pytest_schema.yaml
CHANGED
|
@@ -25,3 +25,22 @@ pytest:
|
|
|
25
25
|
tests:
|
|
26
26
|
type: list
|
|
27
27
|
help: List of test files or directories to include.
|
|
28
|
+
playwright:
|
|
29
|
+
type: object
|
|
30
|
+
help: |
|
|
31
|
+
Settings necessary to also run playwright tests with the pytest
|
|
32
|
+
plugin.
|
|
33
|
+
properties:
|
|
34
|
+
enabled:
|
|
35
|
+
type: bool
|
|
36
|
+
help: |
|
|
37
|
+
Should the pytest-playwright plugin be installed and
|
|
38
|
+
initialized.
|
|
39
|
+
browsers_path:
|
|
40
|
+
type: path
|
|
41
|
+
help: Path for playwright to install the browsers.
|
|
42
|
+
browsers:
|
|
43
|
+
type: list
|
|
44
|
+
help: |
|
|
45
|
+
The browsers to install and to use for running the
|
|
46
|
+
playwright tests.
|
csspin_python/python.py
CHANGED
|
@@ -68,6 +68,7 @@ point to the base installation.
|
|
|
68
68
|
|
|
69
69
|
import abc
|
|
70
70
|
import configparser
|
|
71
|
+
import hashlib
|
|
71
72
|
import logging
|
|
72
73
|
import os
|
|
73
74
|
import re
|
|
@@ -152,10 +153,6 @@ defaults = config(
|
|
|
152
153
|
python="{python.scriptdir}/python{platform.exe}",
|
|
153
154
|
provisioner=None,
|
|
154
155
|
provisioner_memo="{spin.spin_dir}/python_provisioner.memo",
|
|
155
|
-
current_package=config(
|
|
156
|
-
install=True,
|
|
157
|
-
extras=[],
|
|
158
|
-
),
|
|
159
156
|
aws_auth=config(
|
|
160
157
|
enabled=False,
|
|
161
158
|
memo="{spin.spin_dir}/aws_auth.memo",
|
|
@@ -315,13 +312,11 @@ def provision(cfg: ConfigTree) -> None:
|
|
|
315
312
|
"""Provision the python plugin"""
|
|
316
313
|
with memoizer(cfg.python.provisioner_memo) as memo:
|
|
317
314
|
if cfg.python.provisioner is None:
|
|
318
|
-
cfg.python.provisioner = SimpleProvisioner()
|
|
315
|
+
cfg.python.provisioner = SimpleProvisioner(cfg)
|
|
319
316
|
if not memo.check(cfg.python.provisioner):
|
|
320
317
|
memo.add(cfg.python.provisioner)
|
|
321
318
|
|
|
322
|
-
info("Checking {python.interpreter}")
|
|
323
319
|
if not shutil.which(cfg.python.interpreter):
|
|
324
|
-
info("Provisioning '{python.interpreter}'")
|
|
325
320
|
cfg.python.provisioner.provision_python(cfg)
|
|
326
321
|
|
|
327
322
|
venv_provision(cfg)
|
|
@@ -746,7 +741,7 @@ def finalize_provision(cfg: ConfigTree) -> None:
|
|
|
746
741
|
|
|
747
742
|
class ProvisionerProtocol:
|
|
748
743
|
"""An implementation of this protocol is used to provision
|
|
749
|
-
|
|
744
|
+
requirements to a virtual environment.
|
|
750
745
|
|
|
751
746
|
Separate plugins, can implement this interface and overwrite
|
|
752
747
|
cfg.python.provisioner.
|
|
@@ -755,10 +750,8 @@ class ProvisionerProtocol:
|
|
|
755
750
|
The provisioner will be memoized, so make sure it works with ``pickle.dumps``.
|
|
756
751
|
"""
|
|
757
752
|
|
|
758
|
-
|
|
759
|
-
devpackages: set[str]
|
|
753
|
+
_requirements: set[str] = set()
|
|
760
754
|
|
|
761
|
-
# noinspection PyMethodMayBeStatic
|
|
762
755
|
def provision_python(self: Self, cfg: ConfigTree) -> None:
|
|
763
756
|
"""Provision the project's python interpreter"""
|
|
764
757
|
if sys.platform == "win32":
|
|
@@ -770,8 +763,6 @@ class ProvisionerProtocol:
|
|
|
770
763
|
# noinspection PyMethodMayBeStatic
|
|
771
764
|
def provision_venv(self: Self, cfg: ConfigTree) -> None:
|
|
772
765
|
"""Provision the virtual environment of the project"""
|
|
773
|
-
# virtualenv is guaranteed to be available like this
|
|
774
|
-
# as we declared it as one of spin's dependencies
|
|
775
766
|
cmd = [
|
|
776
767
|
sys.executable,
|
|
777
768
|
"-mvirtualenv",
|
|
@@ -790,38 +781,39 @@ class ProvisionerProtocol:
|
|
|
790
781
|
def prerequisites(self: Self, cfg: ConfigTree) -> None:
|
|
791
782
|
"""Provide requirements for the provisioning strategy."""
|
|
792
783
|
|
|
793
|
-
def
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
def sync(self: Self, cfg: ConfigTree) -> None:
|
|
803
|
-
"""Synchronize the environment with the locked dependencies."""
|
|
784
|
+
def add(
|
|
785
|
+
self: Self, cfg: ConfigTree, req: str # pylint: disable=unused-argument
|
|
786
|
+
) -> None:
|
|
787
|
+
"""
|
|
788
|
+
Add a single requirement `req`, that will be installed into the
|
|
789
|
+
environment.
|
|
790
|
+
"""
|
|
791
|
+
self._requirements.add(req)
|
|
804
792
|
|
|
805
793
|
def install(self: Self, cfg: ConfigTree) -> None:
|
|
806
|
-
"""Install the
|
|
794
|
+
"""Install the requirements"""
|
|
807
795
|
|
|
808
|
-
# noinspection PyMethodMayBeStatic
|
|
809
796
|
def cleanup(self: Self, cfg: ConfigTree) -> None:
|
|
810
797
|
"""Cleanup the provisioned environment"""
|
|
811
798
|
rmtree(cfg.python.venv)
|
|
812
799
|
|
|
813
800
|
|
|
814
801
|
class SimpleProvisioner(ProvisionerProtocol):
|
|
815
|
-
"""
|
|
802
|
+
"""
|
|
803
|
+
The simplest Python provisioner, using pip.
|
|
816
804
|
|
|
817
|
-
This provisioner will never uninstall
|
|
818
|
-
|
|
805
|
+
This provisioner will never uninstall requirements that are no longer
|
|
806
|
+
required.
|
|
819
807
|
"""
|
|
820
808
|
|
|
821
|
-
def __init__(self: Self) -> None:
|
|
822
|
-
self.
|
|
823
|
-
self.
|
|
824
|
-
|
|
809
|
+
def __init__(self: Self, cfg: ConfigTree) -> None:
|
|
810
|
+
self._m = Memoizer(interpolate1("{python.memo}"))
|
|
811
|
+
self._install_command = Command(
|
|
812
|
+
"pip",
|
|
813
|
+
None if cfg.verbosity > Verbosity.NORMAL else "-q",
|
|
814
|
+
"--disable-pip-version-check",
|
|
815
|
+
"install",
|
|
816
|
+
)
|
|
825
817
|
|
|
826
818
|
def prerequisites(self: Self, cfg: ConfigTree) -> None:
|
|
827
819
|
# We'll need pip
|
|
@@ -837,81 +829,92 @@ class SimpleProvisioner(ProvisionerProtocol):
|
|
|
837
829
|
"pip",
|
|
838
830
|
)
|
|
839
831
|
|
|
840
|
-
def
|
|
841
|
-
|
|
832
|
+
def install(self: Self, cfg: ConfigTree) -> None:
|
|
833
|
+
if requirements := self._filter(
|
|
834
|
+
self._requirements, self._m, cfg.spin.project_root
|
|
835
|
+
):
|
|
836
|
+
self._install_command(*self._split(requirements))
|
|
837
|
+
self._m.clear()
|
|
838
|
+
for req in requirements:
|
|
839
|
+
self._m.add(_req_for_memo(req, cfg.spin.project_root))
|
|
842
840
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
841
|
+
@staticmethod
|
|
842
|
+
def _split(requirements: Iterable[str]) -> list[str]:
|
|
843
|
+
"""Used to pass whitespace-less args to :func:`csspin.sh()`."""
|
|
844
|
+
requirement_list = []
|
|
845
|
+
for requirement in requirements:
|
|
846
|
+
requirement_list.extend(requirement.split())
|
|
847
|
+
return requirement_list
|
|
848
848
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
849
|
+
@staticmethod
|
|
850
|
+
def _filter(
|
|
851
|
+
requirements: set[str], memo: Memoizer, project_root: Union[Path, str]
|
|
852
|
+
) -> set[str]:
|
|
853
|
+
"""
|
|
854
|
+
We want to filter all requirements prior to installing them, because we
|
|
855
|
+
only want to run the install, when there are changes, as it takes pip
|
|
856
|
+
quite some time to check, whether it has to do something.
|
|
857
|
+
"""
|
|
858
|
+
if all(memo.check(_req_for_memo(req, project_root)) for req in requirements):
|
|
859
|
+
return set()
|
|
860
|
+
else:
|
|
861
|
+
return requirements
|
|
855
862
|
|
|
856
|
-
def install(self: Self, cfg: ConfigTree) -> None:
|
|
857
|
-
quietflag = None if cfg.verbosity > Verbosity.NORMAL else "-q"
|
|
858
|
-
self.__execute_installation(self.devpackages, quietflag, cfg.python.index_url)
|
|
859
863
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
864
|
+
def _file_hash(filename: Union[Path, str]) -> str:
|
|
865
|
+
"""
|
|
866
|
+
Calculate a sha256 hash of a file's content and return its hexdigest.
|
|
867
|
+
"""
|
|
868
|
+
with open(filename, mode="br") as fd:
|
|
869
|
+
return hashlib.sha256(fd.read()).hexdigest() # nosec: hashlib
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def _split_requirement_option(req: str, project_root: Path) -> Union[Path, None]:
|
|
873
|
+
"""
|
|
874
|
+
Takes an element of ``python.requirements`` and checks if it
|
|
875
|
+
is an argument for pip that contains a filename. If so,
|
|
876
|
+
the filename will be returned, ``None`` otherwise.
|
|
877
|
+
|
|
878
|
+
The following options are respected:
|
|
879
|
+
- ``-r``/``--requirement``
|
|
880
|
+
- ``-c``/``--constraint``
|
|
881
|
+
|
|
882
|
+
If a file for an option cannot be found, the plugin will
|
|
883
|
+
:func:`csspin.die()`.
|
|
884
|
+
"""
|
|
885
|
+
if (
|
|
886
|
+
req.startswith(option := "-r")
|
|
887
|
+
or req.startswith(option := "--requirement")
|
|
888
|
+
or req.startswith(option := "-c")
|
|
889
|
+
or req.startswith(option := "--constraint")
|
|
890
|
+
):
|
|
891
|
+
# The pattern has to enforce the " "/"=" for the long-options
|
|
892
|
+
match_many = "+" if option.startswith("--") else "*"
|
|
893
|
+
pattern = rf"{option}[ =]{match_many}(?P<filename>.*)"
|
|
894
|
+
match = re.match(pattern, req)
|
|
895
|
+
if not match:
|
|
896
|
+
die(f"{req} could not be validated.")
|
|
897
|
+
else:
|
|
898
|
+
file = project_root / match.group("filename")
|
|
899
|
+
if not file.exists():
|
|
900
|
+
die(f"{file} does not exist.")
|
|
901
|
+
return file
|
|
902
|
+
return None
|
|
879
903
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
for req in reqset:
|
|
895
|
-
reqlist.extend(req.split())
|
|
896
|
-
return reqlist
|
|
897
|
-
|
|
898
|
-
def __execute_installation(
|
|
899
|
-
self: Self, packages: set[str], quietflag: Union[str, None], index_url: str
|
|
900
|
-
) -> None:
|
|
901
|
-
"""Install packages that are not yet memoized"""
|
|
902
|
-
if to_install := {package for package in packages if not self.m.check(package)}:
|
|
903
|
-
sh(
|
|
904
|
-
"pip",
|
|
905
|
-
quietflag,
|
|
906
|
-
"--disable-pip-version-check",
|
|
907
|
-
"install",
|
|
908
|
-
"--index-url",
|
|
909
|
-
index_url,
|
|
910
|
-
*self._split(to_install),
|
|
911
|
-
)
|
|
912
|
-
for package in to_install:
|
|
913
|
-
self.m.add(package)
|
|
914
|
-
self.m.save()
|
|
904
|
+
|
|
905
|
+
def _req_for_memo(
|
|
906
|
+
req: str, project_root: Union[Path, str]
|
|
907
|
+
) -> str: # pylint: disable=inconsistent-return-statements
|
|
908
|
+
"""
|
|
909
|
+
Return a memoizable representation of a python requirement. In case a
|
|
910
|
+
requirement is on of the following options, the function returns requirement
|
|
911
|
+
with a hash of the files' content appended. Otherwise the requirement itself
|
|
912
|
+
will be returned.
|
|
913
|
+
"""
|
|
914
|
+
if file := _split_requirement_option(req, project_root):
|
|
915
|
+
return f"{req}{_file_hash(file)}"
|
|
916
|
+
else:
|
|
917
|
+
return req
|
|
915
918
|
|
|
916
919
|
|
|
917
920
|
def venv_provision( # pylint: disable=too-many-branches,missing-function-docstring
|
|
@@ -946,16 +949,10 @@ def venv_provision( # pylint: disable=too-many-branches,missing-function-docstr
|
|
|
946
949
|
logging.debug(f"{plugin_module.__name__}.venv_hook()")
|
|
947
950
|
hook(cfg)
|
|
948
951
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
# Install packages required by the project ('requirements')
|
|
952
|
+
# Add packages required by the project ('requirements')
|
|
952
953
|
for req in cfg.python.get("requirements", []):
|
|
953
954
|
cfg.python.provisioner.add(cfg, interpolate1(req))
|
|
954
955
|
|
|
955
|
-
# Install development packages required by the project ('devpackages')
|
|
956
|
-
for pkgspec in cfg.python.get("devpackages", []):
|
|
957
|
-
cfg.python.provisioner.add(cfg, interpolate1(pkgspec), True)
|
|
958
|
-
|
|
959
956
|
# Install packages required by plugins used
|
|
960
957
|
# ('<plugin>.requires.python')
|
|
961
958
|
for plugin in cfg.spin.topo_plugins:
|
|
@@ -963,9 +960,6 @@ def venv_provision( # pylint: disable=too-many-branches,missing-function-docstr
|
|
|
963
960
|
for req in get_requires(plugin_module.defaults, "python"):
|
|
964
961
|
cfg.python.provisioner.add(cfg, interpolate1(req))
|
|
965
962
|
|
|
966
|
-
cfg.python.provisioner.lock_extras(cfg)
|
|
967
|
-
cfg.python.provisioner.sync(cfg)
|
|
968
|
-
|
|
969
963
|
|
|
970
964
|
def cleanup(cfg: ConfigTree) -> None:
|
|
971
965
|
"""Remove directories and files generated by the python plugin."""
|
|
@@ -1046,7 +1040,9 @@ def _check_aws_token_validity(cfg: ConfigTree) -> None:
|
|
|
1046
1040
|
for item in memo.items():
|
|
1047
1041
|
if isinstance(item, str) and item.startswith(f"{timestamp_key}:"):
|
|
1048
1042
|
last_time = int(item.split(":", 1)[1])
|
|
1049
|
-
if current_time - last_time <
|
|
1043
|
+
if current_time - last_time < int(
|
|
1044
|
+
interpolate1(cfg.python.aws_auth.key_duration)
|
|
1045
|
+
):
|
|
1050
1046
|
pipconf = _get_pipconf(cfg)
|
|
1051
1047
|
config_parser = configparser.ConfigParser()
|
|
1052
1048
|
config_parser.read(pipconf)
|
|
@@ -1064,12 +1060,18 @@ def _check_aws_token_validity(cfg: ConfigTree) -> None:
|
|
|
1064
1060
|
info("Updating Codeartifact token.")
|
|
1065
1061
|
from urllib.parse import urljoin
|
|
1066
1062
|
|
|
1063
|
+
opts = {
|
|
1064
|
+
"static_oidc": interpolate1(cfg.python.aws_auth.static_oidc).lower()
|
|
1065
|
+
== "true"
|
|
1066
|
+
}
|
|
1067
|
+
if cfg.python.aws_auth.client_id:
|
|
1068
|
+
opts["client_id"] = interpolate1(cfg.python.aws_auth.client_id)
|
|
1069
|
+
if cfg.python.aws_auth.role_arn:
|
|
1070
|
+
opts["aws_role_arn"] = interpolate1(cfg.python.aws_auth.role_arn)
|
|
1071
|
+
|
|
1072
|
+
index_base_url = get_ca_pypi_url_programmatic(**opts)
|
|
1067
1073
|
index_url = urljoin(
|
|
1068
|
-
|
|
1069
|
-
static_oidc=cfg.python.aws_auth.static_oidc
|
|
1070
|
-
)
|
|
1071
|
-
+ "/",
|
|
1072
|
-
cfg.python.aws_auth.index,
|
|
1074
|
+
index_base_url + "/", interpolate1(cfg.python.aws_auth.index)
|
|
1073
1075
|
)
|
|
1074
1076
|
cfg.python.index_url = index_url
|
|
1075
1077
|
_obfuscate_index_url(index_url)
|
csspin_python/python_schema.yaml
CHANGED
|
@@ -96,9 +96,6 @@ python:
|
|
|
96
96
|
site_packages:
|
|
97
97
|
type: path internal
|
|
98
98
|
help: The path to the virtual environments site-packages directory.
|
|
99
|
-
devpackages:
|
|
100
|
-
type: list
|
|
101
|
-
help: A list of packages that will be editable installed
|
|
102
99
|
requirements:
|
|
103
100
|
type: list
|
|
104
101
|
help: |
|
|
@@ -106,18 +103,8 @@ python:
|
|
|
106
103
|
have to be installed into the project's virtual environment.
|
|
107
104
|
This excludes requirements stated as 'install_requires' in
|
|
108
105
|
setup.py/setup.cfg.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
help: Install configuration of the current python project
|
|
112
|
-
properties:
|
|
113
|
-
install:
|
|
114
|
-
type: bool
|
|
115
|
-
help: |
|
|
116
|
-
Property to determine whether the current package should
|
|
117
|
-
get installed or not.
|
|
118
|
-
extras:
|
|
119
|
-
type: list
|
|
120
|
-
help: The extras of the current package to install.
|
|
106
|
+
Note that editable installs with ``-e`` or requirement files
|
|
107
|
+
with ``-r`` can also be used here.
|
|
121
108
|
index_url:
|
|
122
109
|
type: str
|
|
123
110
|
help: |
|
|
@@ -157,3 +144,9 @@ python:
|
|
|
157
144
|
index:
|
|
158
145
|
type: str
|
|
159
146
|
help: The Codeartifact repository index (e.g. "16.0/simple").
|
|
147
|
+
client_id:
|
|
148
|
+
type: str
|
|
149
|
+
help: The OIDC client ID to use.
|
|
150
|
+
role_arn:
|
|
151
|
+
type: str
|
|
152
|
+
help: The role ARN to assume when authenticating.
|
|
@@ -0,0 +1,187 @@
|
|
|
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
|
+
Plugin to replace certain things of the csspin_python.python plugin with the
|
|
20
|
+
tool ``uv``. Can only be used if the ``uv`` extra of csspin_python has been
|
|
21
|
+
installed.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import shutil
|
|
25
|
+
import subprocess
|
|
26
|
+
from typing import Union
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
import tomllib
|
|
30
|
+
except ImportError:
|
|
31
|
+
# Fallback for spin has been installed with python < 3.11
|
|
32
|
+
import tomli as tomllib
|
|
33
|
+
|
|
34
|
+
import tomli_w
|
|
35
|
+
from csspin import Command, Path, Verbosity, config, die, info, interpolate1, setenv
|
|
36
|
+
from csspin.tree import ConfigTree
|
|
37
|
+
|
|
38
|
+
from csspin_python.python import SimpleProvisioner
|
|
39
|
+
|
|
40
|
+
defaults = config(
|
|
41
|
+
enabled=False,
|
|
42
|
+
uv_python_data="{spin.data}/uv_python",
|
|
43
|
+
uv_toml_path="{python.venv}/uv.toml",
|
|
44
|
+
requires=config(
|
|
45
|
+
spin=[
|
|
46
|
+
"csspin_python.python",
|
|
47
|
+
],
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def venv_hook(cfg: ConfigTree) -> None:
|
|
53
|
+
"""Things to do right after venv creation."""
|
|
54
|
+
_configure_uv_toml(cfg)
|
|
55
|
+
setenv(UV_CONFIG_FILE=cfg.uv_provisioner.uv_toml_path)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def configure(cfg: ConfigTree) -> None:
|
|
59
|
+
"""Configure the uv_provisioner plugin."""
|
|
60
|
+
if interpolate1(cfg.uv_provisioner.enabled).lower() == "true":
|
|
61
|
+
cfg.python.provisioner = SimpleUvProvisioner(cfg)
|
|
62
|
+
setenv(
|
|
63
|
+
UV_PYTHON_INSTALL_DIR=interpolate1(cfg.uv_provisioner.uv_python_data),
|
|
64
|
+
)
|
|
65
|
+
if cfg.python.use:
|
|
66
|
+
cfg.python.interpreter = shutil.which(interpolate1(cfg.python.interpreter))
|
|
67
|
+
else:
|
|
68
|
+
if interpreter_path := _get_uv_python(cfg, True):
|
|
69
|
+
cfg.python.interpreter = interpreter_path
|
|
70
|
+
else:
|
|
71
|
+
# No uv provisioned python found, set to an empty string to
|
|
72
|
+
# force provisioning
|
|
73
|
+
cfg.python.interpreter = ""
|
|
74
|
+
|
|
75
|
+
if cfg.python.aws_auth.enabled:
|
|
76
|
+
# In case we use aws_auth the index-url might have changed
|
|
77
|
+
_update_index_url_in_toml(cfg)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _get_uv_python(cfg: ConfigTree, ignore_errors: bool = False) -> Union[Path, None]:
|
|
81
|
+
"""Use uv to find its provisioned python interpreter."""
|
|
82
|
+
# We cannot put this import top-level as "spin cleanup" might not work
|
|
83
|
+
# otherwise.
|
|
84
|
+
from uv import find_uv_bin
|
|
85
|
+
|
|
86
|
+
cmd = [
|
|
87
|
+
find_uv_bin(),
|
|
88
|
+
"python",
|
|
89
|
+
"find",
|
|
90
|
+
"--no-project",
|
|
91
|
+
"--system",
|
|
92
|
+
"--managed-python",
|
|
93
|
+
cfg.python.version,
|
|
94
|
+
]
|
|
95
|
+
try:
|
|
96
|
+
out = subprocess.check_output(
|
|
97
|
+
cmd, encoding="utf-8", stderr=subprocess.DEVNULL if ignore_errors else None
|
|
98
|
+
)
|
|
99
|
+
interpreter = out.strip()
|
|
100
|
+
return Path(interpreter)
|
|
101
|
+
except subprocess.CalledProcessError as ex:
|
|
102
|
+
if not ignore_errors:
|
|
103
|
+
die(ex)
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class SimpleUvProvisioner(SimpleProvisioner):
|
|
108
|
+
"""
|
|
109
|
+
Drop-in replacement for the SimpleProvisioner that uses ``uv`` for creating
|
|
110
|
+
the environment and installing the requirements.
|
|
111
|
+
|
|
112
|
+
Especially when installing Python packages, the ``SimpleUvProvisioner`` is
|
|
113
|
+
much faster than the ``SimpleProvisioner``.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(self, cfg: ConfigTree) -> None:
|
|
117
|
+
super().__init__(cfg)
|
|
118
|
+
|
|
119
|
+
from uv import find_uv_bin
|
|
120
|
+
|
|
121
|
+
uv_bin = find_uv_bin()
|
|
122
|
+
|
|
123
|
+
if cfg.verbosity == Verbosity.QUIET:
|
|
124
|
+
verbosity = "-q"
|
|
125
|
+
elif cfg.verbosity == Verbosity.DEBUG:
|
|
126
|
+
verbosity = "-v"
|
|
127
|
+
else:
|
|
128
|
+
verbosity = None
|
|
129
|
+
|
|
130
|
+
self._uv_cmd = Command(
|
|
131
|
+
uv_bin,
|
|
132
|
+
verbosity,
|
|
133
|
+
)
|
|
134
|
+
self._install_command = Command(
|
|
135
|
+
*self._uv_cmd._cmd,
|
|
136
|
+
"pip",
|
|
137
|
+
"install",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def provision_python(self, cfg: ConfigTree) -> None:
|
|
141
|
+
self._uv_cmd(
|
|
142
|
+
"python",
|
|
143
|
+
"install",
|
|
144
|
+
cfg.python.version,
|
|
145
|
+
"--no-bin",
|
|
146
|
+
"--no-registry",
|
|
147
|
+
)
|
|
148
|
+
cfg.python.interpreter = _get_uv_python(cfg)
|
|
149
|
+
info(f"Using '{cfg.python.interpreter}' as interpreter")
|
|
150
|
+
|
|
151
|
+
def provision_venv(self, cfg: ConfigTree) -> None:
|
|
152
|
+
setenv(UV_PROJECT_ENVIRONMENT=cfg.python.venv)
|
|
153
|
+
self._uv_cmd(
|
|
154
|
+
"venv",
|
|
155
|
+
f"--python={cfg.python.interpreter}",
|
|
156
|
+
cfg.python.venv,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def prerequisites(self, cfg: ConfigTree) -> None:
|
|
160
|
+
self._uv_cmd("pip", "install", "pip")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _configure_uv_toml(cfg: ConfigTree) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Create a config file for uv, similar to the pip.conf of
|
|
166
|
+
csspin_python.python, since `uv` pip won't respect the pip.conf.
|
|
167
|
+
"""
|
|
168
|
+
toml_content = tomllib.loads(cfg.uv_provisioner.uv_toml or "")
|
|
169
|
+
if "index-url" not in toml_content:
|
|
170
|
+
toml_content["index-url"] = cfg.python.index_url
|
|
171
|
+
else:
|
|
172
|
+
toml_content["index-url"] = toml_content.get("index-url", cfg.python.index_url)
|
|
173
|
+
|
|
174
|
+
with open(cfg.uv_provisioner.uv_toml_path, mode="wb") as fd:
|
|
175
|
+
tomli_w.dump(toml_content, fd)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _update_index_url_in_toml(cfg: ConfigTree) -> None:
|
|
179
|
+
"""
|
|
180
|
+
Update the index-url in the uv.toml in case it changed.
|
|
181
|
+
"""
|
|
182
|
+
if (uv_toml_path := interpolate1(cfg.uv_provisioner.uv_toml_path)).exists():
|
|
183
|
+
with open(uv_toml_path, mode="r+b") as fd:
|
|
184
|
+
toml_content = tomllib.load(fd)
|
|
185
|
+
if toml_content.get("index-url") != cfg.python.index_url:
|
|
186
|
+
toml_content["index-url"] = cfg.python.index_url
|
|
187
|
+
tomli_w.dump(toml_content, fd)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# -*- mode: yaml; coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Schema for the uv_provisioner plugin for spin
|
|
4
|
+
|
|
5
|
+
uv_provisioner:
|
|
6
|
+
type: object
|
|
7
|
+
help: Configuration of the csspin_python.uv plugin
|
|
8
|
+
properties:
|
|
9
|
+
enabled:
|
|
10
|
+
type: bool
|
|
11
|
+
help: |
|
|
12
|
+
Used to let the python plugin use the SimpleUvProvisioner to
|
|
13
|
+
provision the python environment.
|
|
14
|
+
uv_python_data:
|
|
15
|
+
type: path
|
|
16
|
+
help: |
|
|
17
|
+
The directory where the python interpreters uv provisions are
|
|
18
|
+
being stored.
|
|
19
|
+
uv_toml_path:
|
|
20
|
+
type: path
|
|
21
|
+
help: Path to uv's config file
|
|
22
|
+
uv_toml:
|
|
23
|
+
type: str
|
|
24
|
+
help: Content for uv's config file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: csspin-python
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.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>
|
|
@@ -24,6 +24,10 @@ Requires-Dist: platformdirs~=4.3.8
|
|
|
24
24
|
Requires-Dist: virtualenv
|
|
25
25
|
Provides-Extra: aws-auth
|
|
26
26
|
Requires-Dist: csaccess>=0.1.0; extra == "aws-auth"
|
|
27
|
+
Provides-Extra: uv
|
|
28
|
+
Requires-Dist: tomli; python_version < "3.11" and extra == "uv"
|
|
29
|
+
Requires-Dist: tomli-w; extra == "uv"
|
|
30
|
+
Requires-Dist: uv; extra == "uv"
|
|
27
31
|
Dynamic: license-file
|
|
28
32
|
|
|
29
33
|
|Latest Version| |Python| |License|
|
|
@@ -36,16 +40,20 @@ The following plugins are available:
|
|
|
36
40
|
- `csspin_python.behave`: A plugin for running tests using Behave.
|
|
37
41
|
- `csspin_python.debugpy`: A plugin for debugging Python code using `debugpy`_.
|
|
38
42
|
- `csspin_python.devpi`: A plugin for simplified usage of `devpi`_.
|
|
39
|
-
- `csspin_python.playwright`: A plugin for running tests using `playwright`_.
|
|
40
43
|
- `csspin_python.pytest`: A plugin for running tests using pytest.
|
|
41
44
|
- `csspin_python.python`: A plugin for provisioning Python environments and
|
|
42
45
|
installing dependencies.
|
|
43
46
|
- `csspin_python.radon`: A plugin for running `radon`_ to analyze code
|
|
44
47
|
complexity.
|
|
45
48
|
- `csspin_python.sphinx`: A plugin for building Sphinx documentation.
|
|
49
|
+
- `csspin_python.playwright`: A plugin for running tests using `playwright`_.
|
|
50
|
+
This plugin is deprecated, use the pytest plugin with the
|
|
51
|
+
'pytest.playwright.enabled=true' setting instead.
|
|
52
|
+
- `csspin_python.uv_provisioner`: A plugin that uses `uv`_ to provision the Python environment.
|
|
46
53
|
|
|
47
|
-
The package provides an ``aws_auth`` extra, that, if enabled, can
|
|
48
|
-
|
|
54
|
+
The package provides an ``aws_auth`` extra, that, if enabled, can authenticate
|
|
55
|
+
to `CONTACT Software GmbH`_'s AWS Codeartifact. It also provides an ``uv``
|
|
56
|
+
extra, that is necessary for using the ``csspin_python.uv_provisioner`` plugin.
|
|
49
57
|
|
|
50
58
|
Prerequisites
|
|
51
59
|
-------------
|
|
@@ -108,3 +116,4 @@ tests using ``spin pytest`` and do other great things.
|
|
|
108
116
|
.. _`devpi`: https://pypi.org/project/devpi
|
|
109
117
|
.. _`playwright`: https://pypi.org/project/pytest-playwright
|
|
110
118
|
.. _`radon`: https://pypi.org/project/radon
|
|
119
|
+
.. _`uv`: https://docs.astral.sh/uv/
|
|
@@ -0,0 +1,21 @@
|
|
|
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=oFfphLqa4AB6K9vasCUFHN0kFXu63n3ocrsqVuRp4-0,5102
|
|
8
|
+
csspin_python/playwright_schema.yaml,sha256=TSeR16YHa7m7bfO59F2eMV-jXcglluTJdEpUeL16saY,1178
|
|
9
|
+
csspin_python/pytest.py,sha256=pTOb5zFd9RINZwJsHNaRuSGVDkPMABzaAhwpAJo1nQE,4574
|
|
10
|
+
csspin_python/pytest_schema.yaml,sha256=tzXtdF6MvGC9v59EVRJFfLeMMHqPsXcFXy2zJtRECBI,1535
|
|
11
|
+
csspin_python/python.py,sha256=SRsl7wXkPFxEqwhjDtr-Uq80N_JNRrlD0kPMaE1gfrU,34610
|
|
12
|
+
csspin_python/python_schema.yaml,sha256=s8snEDJ8UdfpORgCgqbKvy0exaXlvy4U1gUwBd-Do94,5739
|
|
13
|
+
csspin_python/radon.py,sha256=uFqm6FEi5oWj-_XVaAm3s9cam0cUmr1_FwRf40K6xWs,1876
|
|
14
|
+
csspin_python/radon_schema.yaml,sha256=rlRzXw5z4XbjOVznRiUxWGP4E9hx1Jm-gGw1iQiYzE0,548
|
|
15
|
+
csspin_python/uv_provisioner.py,sha256=A6Di0ahCrZCO3KhYedry7JCfa5ME6h7vH4ypRBZh5UA,5907
|
|
16
|
+
csspin_python/uv_provisioner_schema.yaml,sha256=Y8ZNC2OMnhR8Us3WUXAXK9hMjqGWAKFJB2puX4X5XNQ,727
|
|
17
|
+
csspin_python-3.0.1.dist-info/licenses/LICENSE,sha256=4MAecetnRTQw5DlHtiikDSzKWO1xVLwzM5_DsPMYlnE,10172
|
|
18
|
+
csspin_python-3.0.1.dist-info/METADATA,sha256=jZxpZnrvJ_Afr6Hab4bgRqU0a7TkdjGeOV_Xoy6gqN4,4715
|
|
19
|
+
csspin_python-3.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
20
|
+
csspin_python-3.0.1.dist-info/top_level.txt,sha256=QSeglMEGbFu1z4L6MCQYwo01NgL0KojWvC4rzgMQ8gU,14
|
|
21
|
+
csspin_python-3.0.1.dist-info/RECORD,,
|
|
@@ -1,19 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|