quickpub 4.1.4__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.
- quickpub/__init__.py +10 -0
- quickpub/__main__.py +276 -0
- quickpub/classifiers.py +138 -0
- quickpub/dist_files.py +33 -0
- quickpub/enforcers.py +26 -0
- quickpub/files.py +246 -0
- quickpub/logging_.py +74 -0
- quickpub/package_manager_detection.py +43 -0
- quickpub/proxy.py +35 -0
- quickpub/py.typed +0 -0
- quickpub/qa.py +409 -0
- quickpub/strategies/__init__.py +8 -0
- quickpub/strategies/build_schema.py +21 -0
- quickpub/strategies/constraint_enforcer.py +17 -0
- quickpub/strategies/implementations/__init__.py +6 -0
- quickpub/strategies/implementations/build_schemas/__init__.py +2 -0
- quickpub/strategies/implementations/build_schemas/setuptools_build_schema.py +48 -0
- quickpub/strategies/implementations/build_schemas/uv_build_schema.py +50 -0
- quickpub/strategies/implementations/constraint_enforcers/__init__.py +5 -0
- quickpub/strategies/implementations/constraint_enforcers/license_enforcer.py +29 -0
- quickpub/strategies/implementations/constraint_enforcers/local_version_enforcer.py +80 -0
- quickpub/strategies/implementations/constraint_enforcers/pypi_remote_version_enforcer.py +88 -0
- quickpub/strategies/implementations/constraint_enforcers/pypirc_enforcer.py +45 -0
- quickpub/strategies/implementations/constraint_enforcers/readme_enforcer.py +29 -0
- quickpub/strategies/implementations/package_managers/__init__.py +2 -0
- quickpub/strategies/implementations/package_managers/installed_package_parser.py +29 -0
- quickpub/strategies/implementations/package_managers/pip_package_manager.py +31 -0
- quickpub/strategies/implementations/package_managers/uv_package_manager.py +45 -0
- quickpub/strategies/implementations/python_providers/__init__.py +4 -0
- quickpub/strategies/implementations/python_providers/conda_python_provider.py +54 -0
- quickpub/strategies/implementations/python_providers/default_python_provider.py +47 -0
- quickpub/strategies/implementations/python_providers/union_provider.py +52 -0
- quickpub/strategies/implementations/python_providers/uv_python_provider.py +39 -0
- quickpub/strategies/implementations/quality_assurance_runners/__init__.py +4 -0
- quickpub/strategies/implementations/quality_assurance_runners/mypy_qa_runner.py +98 -0
- quickpub/strategies/implementations/quality_assurance_runners/pylint_qa_runner.py +106 -0
- quickpub/strategies/implementations/quality_assurance_runners/pytest_qa_runner.py +188 -0
- quickpub/strategies/implementations/quality_assurance_runners/unittest_qa_runner.py +138 -0
- quickpub/strategies/implementations/upload_targets/__init__.py +2 -0
- quickpub/strategies/implementations/upload_targets/github_upload_target.py +61 -0
- quickpub/strategies/implementations/upload_targets/pypirc_upload_target.py +90 -0
- quickpub/strategies/package_manager.py +54 -0
- quickpub/strategies/python_provider.py +60 -0
- quickpub/strategies/quality_assurance_runner.py +208 -0
- quickpub/strategies/quickpub_strategy.py +14 -0
- quickpub/strategies/upload_target.py +19 -0
- quickpub/structures/__init__.py +3 -0
- quickpub/structures/bound.py +52 -0
- quickpub/structures/dependency.py +82 -0
- quickpub/structures/version.py +41 -0
- quickpub/validators.py +87 -0
- quickpub/worker_pool.py +16 -0
- quickpub-4.1.4.dist-info/METADATA +246 -0
- quickpub-4.1.4.dist-info/RECORD +58 -0
- quickpub-4.1.4.dist-info/WHEEL +5 -0
- quickpub-4.1.4.dist-info/entry_points.txt +2 -0
- quickpub-4.1.4.dist-info/licenses/LICENSE +21 -0
- quickpub-4.1.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
from typing import Tuple, Set, List, AsyncIterator
|
|
4
|
+
|
|
5
|
+
from .quickpub_strategy import QuickpubStrategy
|
|
6
|
+
from danielutils.async_.async_layered_command import AsyncLayeredCommand
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PythonProvider(AsyncIterator, QuickpubStrategy):
|
|
12
|
+
"""Base class for Python environment providers. Subclass this to define custom Python environment management strategies."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
auto_install_dependencies: bool = True,
|
|
17
|
+
*,
|
|
18
|
+
requested_envs: List[str],
|
|
19
|
+
explicit_versions: List[str],
|
|
20
|
+
exit_on_fail: bool = False,
|
|
21
|
+
) -> None:
|
|
22
|
+
self.auto_install_dependencies = auto_install_dependencies
|
|
23
|
+
self.requested_envs = requested_envs
|
|
24
|
+
self.explicit_versions = explicit_versions
|
|
25
|
+
self.exit_on_fail = exit_on_fail
|
|
26
|
+
self.aiter_index = 0
|
|
27
|
+
|
|
28
|
+
def __aiter__(self) -> AsyncIterator[Tuple[str, AsyncLayeredCommand]]:
|
|
29
|
+
self.aiter_index = 0
|
|
30
|
+
return self
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
async def __anext__(self) -> Tuple[str, AsyncLayeredCommand]: ...
|
|
34
|
+
|
|
35
|
+
async def _get_available_envs(self) -> Set[str]:
|
|
36
|
+
KEY = "__available_envs__"
|
|
37
|
+
if (res := getattr(self, KEY, None)) is not None:
|
|
38
|
+
logger.debug(
|
|
39
|
+
"Using cached available environments for %s", self.__class__.__name__
|
|
40
|
+
)
|
|
41
|
+
return res
|
|
42
|
+
|
|
43
|
+
logger.debug("Fetching available environments for %s", self.__class__.__name__)
|
|
44
|
+
setattr(self, KEY, res := await self._get_available_envs_impl())
|
|
45
|
+
logger.debug(
|
|
46
|
+
"Found %d available environments for %s", len(res), self.__class__.__name__
|
|
47
|
+
)
|
|
48
|
+
return res
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
async def _get_available_envs_impl(self) -> Set[str]: ...
|
|
52
|
+
|
|
53
|
+
def __len__(self) -> int:
|
|
54
|
+
return len(self.requested_envs)
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def get_python_executable_name(self) -> str: ...
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
__all__ = ["PythonProvider"]
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from abc import abstractmethod
|
|
6
|
+
from typing import Union, List, Optional, cast, Dict, Tuple, TYPE_CHECKING
|
|
7
|
+
from danielutils import LayeredCommand, file_exists
|
|
8
|
+
from danielutils.async_.async_layered_command import AsyncLayeredCommand
|
|
9
|
+
|
|
10
|
+
from quickpub import Bound
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .package_manager import PackageManager
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Configurable:
|
|
19
|
+
@property
|
|
20
|
+
def has_config(self) -> bool:
|
|
21
|
+
return self.config_path is not None
|
|
22
|
+
|
|
23
|
+
def __init__(self, config_path: Optional[str] = None):
|
|
24
|
+
self.config_path = config_path
|
|
25
|
+
if self.has_config:
|
|
26
|
+
logger.debug("Using configuration file: %s", self.config_path)
|
|
27
|
+
if not file_exists(self.config_path):
|
|
28
|
+
logger.error("Configuration file not found: %s", self.config_path)
|
|
29
|
+
raise FileNotFoundError(f"Can't find config file {self.config_path}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class HasOptionalExecutable:
|
|
33
|
+
PYTHON: str = sys.executable
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def use_executable(self) -> bool:
|
|
37
|
+
return self.executable_path is not None
|
|
38
|
+
|
|
39
|
+
def __init__(self, name: str, executable_path: Optional[str] = None) -> None:
|
|
40
|
+
self.name = name
|
|
41
|
+
self.executable_path = executable_path
|
|
42
|
+
if self.use_executable:
|
|
43
|
+
logger.debug("Using custom executable: %s", self.executable_path)
|
|
44
|
+
if not file_exists(self.executable_path):
|
|
45
|
+
logger.error("Executable not found: %s", self.executable_path)
|
|
46
|
+
raise FileNotFoundError(f"Executable not found {self.executable_path}")
|
|
47
|
+
else:
|
|
48
|
+
logger.debug("Using system executable for: %s", name)
|
|
49
|
+
|
|
50
|
+
def get_executable(self, use_system_interpreter: bool = False) -> str:
|
|
51
|
+
if self.use_executable:
|
|
52
|
+
return cast(str, self.executable_path)
|
|
53
|
+
|
|
54
|
+
p = self.PYTHON
|
|
55
|
+
if use_system_interpreter:
|
|
56
|
+
p = sys.executable
|
|
57
|
+
return f"{p} -m {self.name}"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
SPEICLA_EXIT_CODES: Dict[int, Tuple[str, str]] = {
|
|
61
|
+
-1073741515: (
|
|
62
|
+
"Can't find python in path.",
|
|
63
|
+
"Executing command '{command}' failed with exit code {ret} which in hex is {hex} which corresponds to STATUS_DLL_NOT_FOUND",
|
|
64
|
+
),
|
|
65
|
+
3221225781: (
|
|
66
|
+
"Can't find python in path.",
|
|
67
|
+
"Executing command '{command}' failed with exit code {ret} which in hex is {hex} which corresponds to STATUS_DLL_NOT_FOUND",
|
|
68
|
+
),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class QualityAssuranceRunner(Configurable, HasOptionalExecutable):
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
*,
|
|
76
|
+
name: str,
|
|
77
|
+
bound: Union[str, Bound],
|
|
78
|
+
target: Optional[str] = None,
|
|
79
|
+
configuration_path: Optional[str] = None,
|
|
80
|
+
executable_path: Optional[str] = None,
|
|
81
|
+
package_manager: Optional["PackageManager"] = None,
|
|
82
|
+
) -> None:
|
|
83
|
+
from .package_manager import PackageManager as PackageManagerType
|
|
84
|
+
from .implementations.package_managers.pip_package_manager import (
|
|
85
|
+
PipPackageManager,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
Configurable.__init__(self, configuration_path)
|
|
89
|
+
HasOptionalExecutable.__init__(self, name, executable_path)
|
|
90
|
+
self.package_manager: PackageManagerType = (
|
|
91
|
+
package_manager if package_manager is not None else PipPackageManager()
|
|
92
|
+
)
|
|
93
|
+
self.bound: Bound = (
|
|
94
|
+
bound if isinstance(bound, Bound) else Bound.from_string(bound)
|
|
95
|
+
)
|
|
96
|
+
self.target = target
|
|
97
|
+
logger.debug(
|
|
98
|
+
"QualityAssuranceRunner '%s' initialized with bound=%s, target=%s",
|
|
99
|
+
name,
|
|
100
|
+
self.bound,
|
|
101
|
+
target,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
@abstractmethod
|
|
105
|
+
def _build_command(
|
|
106
|
+
self, target: str, use_system_interpreter: bool = False
|
|
107
|
+
) -> str: ...
|
|
108
|
+
|
|
109
|
+
@abstractmethod
|
|
110
|
+
def _install_dependencies(self, base: LayeredCommand) -> None: ...
|
|
111
|
+
|
|
112
|
+
def _pre_command(self) -> None: ...
|
|
113
|
+
|
|
114
|
+
def _post_command(self) -> None: ...
|
|
115
|
+
|
|
116
|
+
def _handle_special_exit_codes(self, ret: int, command: str) -> None:
|
|
117
|
+
if ret in SPEICLA_EXIT_CODES:
|
|
118
|
+
title, explanation = SPEICLA_EXIT_CODES[ret]
|
|
119
|
+
unsigned_integer_ret = ret + 2**32
|
|
120
|
+
logger.error("Special exit code %d encountered: %s", ret, title)
|
|
121
|
+
raise RuntimeError(
|
|
122
|
+
title
|
|
123
|
+
+ "\n\t"
|
|
124
|
+
+ explanation.format(
|
|
125
|
+
command=command, ret=ret, hex=hex(unsigned_integer_ret)
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def _validate_score_against_bound(
|
|
130
|
+
self, score: float, env_name: str, verbose: bool = False
|
|
131
|
+
) -> None:
|
|
132
|
+
from quickpub.enforcers import exit_if # pylint: disable=import-error
|
|
133
|
+
|
|
134
|
+
logger.debug(
|
|
135
|
+
"QA runner '%s' scored %.3f (bound: %s)",
|
|
136
|
+
self.__class__.__name__,
|
|
137
|
+
score,
|
|
138
|
+
self.bound,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if not self.bound.compare_against(score):
|
|
142
|
+
logger.error(
|
|
143
|
+
"QA runner '%s' failed bound check: %s vs %s",
|
|
144
|
+
self.__class__.__name__,
|
|
145
|
+
score,
|
|
146
|
+
self.bound,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
exit_if(
|
|
150
|
+
not self.bound.compare_against(score),
|
|
151
|
+
f"On env '{env_name}' runner '{self.__class__.__name__}' failed to pass its defined bound. Got a score of {score} but expected {self.bound}",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
async def run(
|
|
155
|
+
self,
|
|
156
|
+
target: str,
|
|
157
|
+
executor: AsyncLayeredCommand,
|
|
158
|
+
*,
|
|
159
|
+
verbose: bool = True, # type: ignore
|
|
160
|
+
use_system_interpreter: bool = False,
|
|
161
|
+
env_name: str,
|
|
162
|
+
) -> None:
|
|
163
|
+
logger.debug(
|
|
164
|
+
"Running %s on environment '%s' with target '%s'",
|
|
165
|
+
self.__class__.__name__,
|
|
166
|
+
env_name,
|
|
167
|
+
target,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
command = self._build_command(target, use_system_interpreter)
|
|
171
|
+
command = self.package_manager.wrap_command(command)
|
|
172
|
+
logger.debug("Built command: %s", command)
|
|
173
|
+
|
|
174
|
+
self._pre_command()
|
|
175
|
+
start_time = time.perf_counter()
|
|
176
|
+
try:
|
|
177
|
+
ret, out, err = await executor(command, command_raise_on_fail=False)
|
|
178
|
+
self._handle_special_exit_codes(ret, command)
|
|
179
|
+
|
|
180
|
+
score = self._calculate_score(ret, out + err, verbose=verbose)
|
|
181
|
+
self._validate_score_against_bound(score, env_name, verbose)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.error(
|
|
184
|
+
"QA runner '%s' failed on env '%s': %s",
|
|
185
|
+
self.__class__.__name__,
|
|
186
|
+
env_name,
|
|
187
|
+
e,
|
|
188
|
+
)
|
|
189
|
+
raise RuntimeError(
|
|
190
|
+
f"On env {env_name}, failed to run {self.__class__.__name__}. Try running manually:\n{executor._build_command(command)}",
|
|
191
|
+
e,
|
|
192
|
+
) from e
|
|
193
|
+
finally:
|
|
194
|
+
elapsed = time.perf_counter() - start_time
|
|
195
|
+
logger.info(
|
|
196
|
+
"QA runner '%s' finished in %.3fs",
|
|
197
|
+
self.__class__.__name__,
|
|
198
|
+
elapsed,
|
|
199
|
+
)
|
|
200
|
+
self._post_command()
|
|
201
|
+
|
|
202
|
+
@abstractmethod
|
|
203
|
+
def _calculate_score(
|
|
204
|
+
self, ret: int, command_output: List[str], *, verbose: bool = False
|
|
205
|
+
) -> float: ...
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
__all__ = ["QualityAssuranceRunner"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
|
|
3
|
+
from danielutils.university.oop.strategy import Strategy
|
|
4
|
+
|
|
5
|
+
from ..enforcers import ExitEarlyError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class QuickpubStrategy(Strategy):
|
|
9
|
+
EXCEPTION_TYPE: Type[Exception] = ExitEarlyError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"QuickpubStrategy",
|
|
14
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from .quickpub_strategy import QuickpubStrategy
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class UploadTarget(QuickpubStrategy):
|
|
8
|
+
"""Base class for upload target implementations. Subclass this to define custom upload strategies."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, verbose: bool = True) -> None:
|
|
11
|
+
self.verbose = verbose
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def upload(self, **kwargs: Any) -> None: ...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"UploadTarget",
|
|
19
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Literal, List
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Bound:
|
|
10
|
+
operator: Literal["<", "<=", "==", ">", ">="]
|
|
11
|
+
value: float
|
|
12
|
+
|
|
13
|
+
def compare_against(self, score: float) -> bool:
|
|
14
|
+
result = {
|
|
15
|
+
">": score > self.value,
|
|
16
|
+
">=": score >= self.value,
|
|
17
|
+
"<": score < self.value,
|
|
18
|
+
"<=": score <= self.value,
|
|
19
|
+
"==": score == self.value,
|
|
20
|
+
}[self.operator]
|
|
21
|
+
logger.debug(
|
|
22
|
+
"Bound comparison: %s %s %s = %s", score, self.operator, self.value, result
|
|
23
|
+
)
|
|
24
|
+
return result
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def from_string(s: str) -> "Bound":
|
|
28
|
+
logger.debug("Parsing bound from string: '%s'", s)
|
|
29
|
+
valid_operators: List[Literal["<", "<=", "==", ">", ">="]] = [
|
|
30
|
+
">=",
|
|
31
|
+
"<=",
|
|
32
|
+
"==",
|
|
33
|
+
">",
|
|
34
|
+
"<",
|
|
35
|
+
]
|
|
36
|
+
for op in valid_operators:
|
|
37
|
+
splits = s.split(op)
|
|
38
|
+
if len(splits) == 2:
|
|
39
|
+
bound = Bound(op, float(splits[-1]))
|
|
40
|
+
logger.debug("Parsed bound: %s", bound)
|
|
41
|
+
return bound
|
|
42
|
+
logger.error("Failed to parse bound from string: '%s'", s)
|
|
43
|
+
raise ValueError("Invalid 'Bound' format")
|
|
44
|
+
|
|
45
|
+
def __str__(self) -> str:
|
|
46
|
+
return f"{self.operator}{self.value}"
|
|
47
|
+
|
|
48
|
+
def __repr__(self) -> str:
|
|
49
|
+
return f"{self.__class__.__name__}(operator='{self.operator}', value='{self.value}')"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
__all__ = ["Bound"]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Literal, Callable, Dict, Any, List
|
|
3
|
+
|
|
4
|
+
from .version import Version
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Dependency:
|
|
10
|
+
"""Represents a package dependency with version constraints. Use from_string() to parse dependency strings."""
|
|
11
|
+
|
|
12
|
+
def _build_func_map(self) -> Dict[str, Callable[[Version], bool]]:
|
|
13
|
+
return {
|
|
14
|
+
"==": lambda v: v == self.ver,
|
|
15
|
+
">=": lambda v: v >= self.ver,
|
|
16
|
+
"<=": lambda v: v <= self.ver,
|
|
17
|
+
">": lambda v: v > self.ver,
|
|
18
|
+
"<": lambda v: v < self.ver,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
name: str,
|
|
24
|
+
operator: Literal["<", "<=", "==", ">", ">="] = ">=",
|
|
25
|
+
ver: Version = Version(0, 0, 0),
|
|
26
|
+
) -> None:
|
|
27
|
+
self.name: str = name
|
|
28
|
+
self.operator: Literal["<", "<=", "==", ">", ">="] = operator
|
|
29
|
+
self.ver: Version = ver or Version(0, 0, 0)
|
|
30
|
+
logger.debug("Dependency created: %s %s %s", self.name, self.operator, self.ver)
|
|
31
|
+
|
|
32
|
+
def __eq__(self, other: Any) -> bool:
|
|
33
|
+
if not isinstance(other, Dependency):
|
|
34
|
+
return False
|
|
35
|
+
return (
|
|
36
|
+
self.name == other.name
|
|
37
|
+
and self.operator == other.operator
|
|
38
|
+
and self.ver == other.ver
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def __hash__(self) -> int:
|
|
42
|
+
return hash((self.name, self.operator, self.ver))
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def from_string(s: str) -> "Dependency":
|
|
46
|
+
logger.debug("Parsing dependency from string: '%s'", s)
|
|
47
|
+
valid_operators: List[Literal["<", "<=", "==", ">", ">="]] = [
|
|
48
|
+
">=",
|
|
49
|
+
"<=",
|
|
50
|
+
">",
|
|
51
|
+
"<",
|
|
52
|
+
"==",
|
|
53
|
+
]
|
|
54
|
+
for op in valid_operators:
|
|
55
|
+
splits = s.split(op)
|
|
56
|
+
if len(splits) == 2:
|
|
57
|
+
dep = Dependency(
|
|
58
|
+
splits[0],
|
|
59
|
+
op,
|
|
60
|
+
Version.from_str(splits[-1]),
|
|
61
|
+
)
|
|
62
|
+
logger.debug("Parsed dependency: %s", dep)
|
|
63
|
+
return dep
|
|
64
|
+
dep = Dependency(s, ">=", Version(0, 0, 0))
|
|
65
|
+
logger.debug("Parsed dependency (default): %s", dep)
|
|
66
|
+
return dep
|
|
67
|
+
|
|
68
|
+
def __str__(self) -> str:
|
|
69
|
+
if self.ver == Version(0, 0, 0):
|
|
70
|
+
return self.name
|
|
71
|
+
return f"{self.name}{self.operator}{self.ver}"
|
|
72
|
+
|
|
73
|
+
def __repr__(self) -> str:
|
|
74
|
+
return f"{self.__class__.__name__}(name='{self.name}', operator='{self.operator}', version='{self.ver}')"
|
|
75
|
+
|
|
76
|
+
def is_satisfied_by(self, ver: Version) -> bool:
|
|
77
|
+
result = self._build_func_map()[self.operator](ver)
|
|
78
|
+
logger.debug("Dependency '%s' satisfied by version '%s': %s", self, ver, result)
|
|
79
|
+
return result
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
__all__ = ["Dependency"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
logger = logging.getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(order=True)
|
|
8
|
+
class Version:
|
|
9
|
+
@staticmethod
|
|
10
|
+
def from_str(version_str: str) -> "Version":
|
|
11
|
+
try:
|
|
12
|
+
version = Version(*list(map(int, version_str.split("."))))
|
|
13
|
+
return version
|
|
14
|
+
except Exception as e:
|
|
15
|
+
logger.error("Failed to parse version from string '%s': %s", version_str, e)
|
|
16
|
+
raise ValueError(
|
|
17
|
+
f"Failed converting '{version_str}' to instance of 'Version' in 'Version.from_str"
|
|
18
|
+
) from e
|
|
19
|
+
|
|
20
|
+
major: int = 0
|
|
21
|
+
minor: int = 0
|
|
22
|
+
patch: int = 0
|
|
23
|
+
|
|
24
|
+
def __init__(self, major: int = 0, minor: int = 0, patch: int = 0) -> None:
|
|
25
|
+
if not all(map(lambda x: isinstance(x, int) and x >= 0, [major, minor, patch])):
|
|
26
|
+
logger.error(
|
|
27
|
+
"Invalid version components: major=%s, minor=%s, patch=%s",
|
|
28
|
+
major,
|
|
29
|
+
minor,
|
|
30
|
+
patch,
|
|
31
|
+
)
|
|
32
|
+
raise ValueError("Version supports positive integers only")
|
|
33
|
+
self.major = major
|
|
34
|
+
self.minor = minor
|
|
35
|
+
self.patch = patch
|
|
36
|
+
|
|
37
|
+
def __str__(self) -> str:
|
|
38
|
+
return f"{self.major}.{self.minor}.{self.patch}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
__all__ = ["Version"]
|
quickpub/validators.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional, Union, List
|
|
3
|
+
|
|
4
|
+
from danielutils import get_python_version
|
|
5
|
+
|
|
6
|
+
from .enforcers import ExitEarlyError
|
|
7
|
+
from .structures import Version, Dependency
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def validate_version(version: Optional[Union[str, Version]] = None) -> Version:
|
|
13
|
+
logger.debug("Validating version: %s", version)
|
|
14
|
+
if not bool(version):
|
|
15
|
+
logger.error("Version validation failed: no version provided")
|
|
16
|
+
raise ExitEarlyError(f"Must supply a version number. got '{version}'")
|
|
17
|
+
|
|
18
|
+
if isinstance(version, Version):
|
|
19
|
+
logger.debug("Version is already a Version object: %s", version)
|
|
20
|
+
return version
|
|
21
|
+
logger.debug("Converting string version to Version object: %s", version)
|
|
22
|
+
if not isinstance(version, str):
|
|
23
|
+
raise ExitEarlyError(
|
|
24
|
+
f"Version must be a string or Version object, got '{type(version)}'"
|
|
25
|
+
)
|
|
26
|
+
return Version.from_str(version)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def validate_python_version(min_python: Optional[Version]) -> Version:
|
|
30
|
+
logger.debug("Validating Python version. min_python: %s", min_python)
|
|
31
|
+
if min_python is not None:
|
|
32
|
+
logger.debug("Using provided minimum Python version: %s", min_python)
|
|
33
|
+
return min_python
|
|
34
|
+
|
|
35
|
+
current_version = Version(*get_python_version())
|
|
36
|
+
logger.debug("Using current Python version: %s", current_version)
|
|
37
|
+
return current_version
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def validate_keywords(keywords: Optional[List[str]]) -> List[str]:
|
|
41
|
+
logger.debug("Validating keywords: %s", keywords)
|
|
42
|
+
if keywords is None:
|
|
43
|
+
logger.debug("No keywords provided, returning empty list")
|
|
44
|
+
return []
|
|
45
|
+
logger.debug("Using provided keywords: %s", keywords)
|
|
46
|
+
return keywords
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def validate_dependencies(
|
|
50
|
+
dependencies: Optional[List[Union[str, Dependency]]],
|
|
51
|
+
) -> List[Dependency]:
|
|
52
|
+
logger.debug("Validating dependencies: %s", dependencies)
|
|
53
|
+
if dependencies is None:
|
|
54
|
+
logger.debug("No dependencies provided, returning empty list")
|
|
55
|
+
return []
|
|
56
|
+
|
|
57
|
+
res = []
|
|
58
|
+
for dep in dependencies:
|
|
59
|
+
if isinstance(dep, str):
|
|
60
|
+
logger.debug("Converting string dependency to Dependency object: %s", dep)
|
|
61
|
+
res.append(Dependency.from_string(dep))
|
|
62
|
+
else:
|
|
63
|
+
logger.debug("Using existing Dependency object: %s", dep)
|
|
64
|
+
res.append(dep)
|
|
65
|
+
|
|
66
|
+
logger.debug("Validated %d dependencies", len(res))
|
|
67
|
+
return res
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def validate_source(name: str, src: Optional[str] = None) -> str:
|
|
71
|
+
logger.debug("Validating source for package '%s'. src: %s", name, src)
|
|
72
|
+
if src is not None:
|
|
73
|
+
logger.debug("Using provided source path: %s", src)
|
|
74
|
+
return src
|
|
75
|
+
|
|
76
|
+
default_src = f"./{name}"
|
|
77
|
+
logger.debug("Using default source path: %s", default_src)
|
|
78
|
+
return default_src
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
"validate_version",
|
|
83
|
+
"validate_python_version",
|
|
84
|
+
"validate_keywords",
|
|
85
|
+
"validate_dependencies",
|
|
86
|
+
"validate_source",
|
|
87
|
+
]
|
quickpub/worker_pool.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
from danielutils import AsyncWorkerPool
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WorkerPool(AsyncWorkerPool):
|
|
9
|
+
@staticmethod
|
|
10
|
+
def log(level: int, message: str, *args: Any, **kwargs: Any) -> None:
|
|
11
|
+
logger.log(level, message, *args, **kwargs)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"WorkerPool",
|
|
16
|
+
]
|