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.
Files changed (58) hide show
  1. quickpub/__init__.py +10 -0
  2. quickpub/__main__.py +276 -0
  3. quickpub/classifiers.py +138 -0
  4. quickpub/dist_files.py +33 -0
  5. quickpub/enforcers.py +26 -0
  6. quickpub/files.py +246 -0
  7. quickpub/logging_.py +74 -0
  8. quickpub/package_manager_detection.py +43 -0
  9. quickpub/proxy.py +35 -0
  10. quickpub/py.typed +0 -0
  11. quickpub/qa.py +409 -0
  12. quickpub/strategies/__init__.py +8 -0
  13. quickpub/strategies/build_schema.py +21 -0
  14. quickpub/strategies/constraint_enforcer.py +17 -0
  15. quickpub/strategies/implementations/__init__.py +6 -0
  16. quickpub/strategies/implementations/build_schemas/__init__.py +2 -0
  17. quickpub/strategies/implementations/build_schemas/setuptools_build_schema.py +48 -0
  18. quickpub/strategies/implementations/build_schemas/uv_build_schema.py +50 -0
  19. quickpub/strategies/implementations/constraint_enforcers/__init__.py +5 -0
  20. quickpub/strategies/implementations/constraint_enforcers/license_enforcer.py +29 -0
  21. quickpub/strategies/implementations/constraint_enforcers/local_version_enforcer.py +80 -0
  22. quickpub/strategies/implementations/constraint_enforcers/pypi_remote_version_enforcer.py +88 -0
  23. quickpub/strategies/implementations/constraint_enforcers/pypirc_enforcer.py +45 -0
  24. quickpub/strategies/implementations/constraint_enforcers/readme_enforcer.py +29 -0
  25. quickpub/strategies/implementations/package_managers/__init__.py +2 -0
  26. quickpub/strategies/implementations/package_managers/installed_package_parser.py +29 -0
  27. quickpub/strategies/implementations/package_managers/pip_package_manager.py +31 -0
  28. quickpub/strategies/implementations/package_managers/uv_package_manager.py +45 -0
  29. quickpub/strategies/implementations/python_providers/__init__.py +4 -0
  30. quickpub/strategies/implementations/python_providers/conda_python_provider.py +54 -0
  31. quickpub/strategies/implementations/python_providers/default_python_provider.py +47 -0
  32. quickpub/strategies/implementations/python_providers/union_provider.py +52 -0
  33. quickpub/strategies/implementations/python_providers/uv_python_provider.py +39 -0
  34. quickpub/strategies/implementations/quality_assurance_runners/__init__.py +4 -0
  35. quickpub/strategies/implementations/quality_assurance_runners/mypy_qa_runner.py +98 -0
  36. quickpub/strategies/implementations/quality_assurance_runners/pylint_qa_runner.py +106 -0
  37. quickpub/strategies/implementations/quality_assurance_runners/pytest_qa_runner.py +188 -0
  38. quickpub/strategies/implementations/quality_assurance_runners/unittest_qa_runner.py +138 -0
  39. quickpub/strategies/implementations/upload_targets/__init__.py +2 -0
  40. quickpub/strategies/implementations/upload_targets/github_upload_target.py +61 -0
  41. quickpub/strategies/implementations/upload_targets/pypirc_upload_target.py +90 -0
  42. quickpub/strategies/package_manager.py +54 -0
  43. quickpub/strategies/python_provider.py +60 -0
  44. quickpub/strategies/quality_assurance_runner.py +208 -0
  45. quickpub/strategies/quickpub_strategy.py +14 -0
  46. quickpub/strategies/upload_target.py +19 -0
  47. quickpub/structures/__init__.py +3 -0
  48. quickpub/structures/bound.py +52 -0
  49. quickpub/structures/dependency.py +82 -0
  50. quickpub/structures/version.py +41 -0
  51. quickpub/validators.py +87 -0
  52. quickpub/worker_pool.py +16 -0
  53. quickpub-4.1.4.dist-info/METADATA +246 -0
  54. quickpub-4.1.4.dist-info/RECORD +58 -0
  55. quickpub-4.1.4.dist-info/WHEEL +5 -0
  56. quickpub-4.1.4.dist-info/entry_points.txt +2 -0
  57. quickpub-4.1.4.dist-info/licenses/LICENSE +21 -0
  58. quickpub-4.1.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,106 @@
1
+ import logging
2
+ import re
3
+ from typing import Optional, List
4
+
5
+ from danielutils import LayeredCommand
6
+
7
+ from ....enforcers import ExitEarlyError
8
+ from ...quality_assurance_runner import QualityAssuranceRunner
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class PylintRunner(QualityAssuranceRunner):
14
+ """Quality assurance runner for pylint code analysis. Scores based on pylint rating (0.0 to 10.0)."""
15
+
16
+ def _install_dependencies(self, base: LayeredCommand) -> None:
17
+ logger.info("Installing pylint dependencies")
18
+ with base:
19
+ base(self.package_manager.install_command("pylint"))
20
+
21
+ RATING_PATTERN: re.Pattern = re.compile(r"([\d.]+)/([\d.]+)")
22
+
23
+ def __init__(
24
+ self,
25
+ bound: str = ">=0.8",
26
+ configuration_path: Optional[str] = None,
27
+ executable_path: Optional[str] = None,
28
+ package_manager=None,
29
+ ) -> None:
30
+ QualityAssuranceRunner.__init__(
31
+ self,
32
+ name="pylint",
33
+ bound=bound,
34
+ configuration_path=configuration_path,
35
+ executable_path=executable_path,
36
+ package_manager=package_manager,
37
+ )
38
+ logger.info(
39
+ "Initialized PylintRunner with bound='%s', config='%s', executable='%s'",
40
+ bound,
41
+ configuration_path,
42
+ executable_path,
43
+ )
44
+
45
+ def _build_command(self, target: str, use_system_interpreter: bool = False) -> str:
46
+ command: str = self.get_executable()
47
+ if self.has_config:
48
+ command += f" --rcfile {self.config_path}"
49
+ command += f" {target}"
50
+ return command
51
+
52
+ def _calculate_score(
53
+ self, ret: int, lines: List[str], verbose: bool = False
54
+ ) -> float:
55
+ logger.debug("Calculating pylint score from analysis results")
56
+
57
+ if len(lines) == 0:
58
+ logger.debug("No pylint output, returning perfect score: 1.0")
59
+ return 1
60
+
61
+ for line in reversed(lines):
62
+ match = self.RATING_PATTERN.search(line)
63
+ if match:
64
+ numerator = float(match.group(1))
65
+ denominator = float(match.group(2))
66
+ score = numerator / denominator
67
+ logger.debug(
68
+ "Pylint score calculated: %.3f (%s/%s)",
69
+ score,
70
+ match.group(1),
71
+ match.group(2),
72
+ )
73
+ return score
74
+
75
+ if ret == 0:
76
+ return 1.0
77
+
78
+ joined_output = "\n".join(lines)
79
+ if ret == 1 and "parse-error" in joined_output:
80
+ logger.debug(
81
+ "Pylint reported parse-error with an empty target, returning perfect score: 1.0"
82
+ )
83
+ return 1.0
84
+
85
+ if len(lines) == 1:
86
+ if lines[0].endswith("No module named pylint"):
87
+ logger.error("Pylint module not found")
88
+ raise ExitEarlyError("No module named pylint found")
89
+
90
+ if lines[0].startswith("The config file") and lines[0].endswith(
91
+ "doesn't exist!"
92
+ ):
93
+ logger.error("Config file error: %s", lines[0])
94
+ raise ExitEarlyError(lines[0])
95
+
96
+ logger.error("Unexpected pylint error: %s", lines[0])
97
+ raise ExitEarlyError(f"Got an unexpected error: {lines[0]}")
98
+
99
+ msg = f"Failed running Pylint, got exit code {ret}. Try running manually using: {self._build_command('TARGET')}"
100
+ logger.error("Failed to parse pylint rating from output: %s", joined_output)
101
+ raise ExitEarlyError(msg)
102
+
103
+
104
+ __all__ = [
105
+ "PylintRunner",
106
+ ]
@@ -0,0 +1,188 @@
1
+ import logging
2
+ import re
3
+ import subprocess
4
+ import sys
5
+ from typing import List, Union, Literal, Optional
6
+
7
+ from danielutils import LayeredCommand
8
+
9
+ from ....enforcers import ExitEarlyError
10
+ from ....structures import Bound
11
+ from ...quality_assurance_runner import QualityAssuranceRunner
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class PytestRunner(QualityAssuranceRunner):
17
+ """Quality assurance runner for pytest testing. Scores based on the ratio of passed tests to total tests."""
18
+
19
+ PYTEST_SUMMARY_REGEX: re.Pattern = re.compile(
20
+ r"=+ .*?in [\d\.]+s(?: \([^)]+\))? =+"
21
+ )
22
+ PYTEST_QUIET_SUMMARY_REGEX: re.Pattern = re.compile(
23
+ r".*\d+ (?:failed|passed).+in [\d\.]+s(?: \([^)]+\))?"
24
+ )
25
+ PYTEST_FAILED_REGEX: re.Pattern = re.compile(r"(\d+) failed")
26
+ PYTEST_PASSED_REGEX: re.Pattern = re.compile(r"(\d+) passed")
27
+ PYTEST_SKIPPED_REGEX: re.Pattern = re.compile(r"(\d+) skipped")
28
+ ANSI_ESCAPE_REGEX: re.Pattern = re.compile(r"\x1b\[[0-9;]*m")
29
+
30
+ def __init__(
31
+ self,
32
+ *,
33
+ bound: Union[str, Bound] = ">=0.8",
34
+ target: str = "./tests",
35
+ no_output_score: float = 0.0,
36
+ no_tests_score: float = 1.0,
37
+ xdist_workers: Union[int, Literal["auto"]] = "auto",
38
+ configuration_path: Optional[str] = None,
39
+ executable_path: Optional[str] = None,
40
+ package_manager=None,
41
+ ) -> None:
42
+ super().__init__(
43
+ name="pytest",
44
+ bound=bound,
45
+ target=target,
46
+ configuration_path=configuration_path,
47
+ executable_path=executable_path,
48
+ package_manager=package_manager,
49
+ )
50
+ if not (0.0 <= no_tests_score <= 1.0):
51
+ raise RuntimeError(
52
+ "no_tests_score should be between 0.0 and 1.0 (including both)."
53
+ )
54
+ self.no_tests_score = no_tests_score
55
+
56
+ if not (0.0 <= no_output_score <= 1.0):
57
+ raise RuntimeError(
58
+ "no_output_score should be between 0.0 and 1.0 (including both)."
59
+ )
60
+ self.no_output_score = no_output_score
61
+ if isinstance(xdist_workers, int) and xdist_workers <= 0:
62
+ raise RuntimeError("xdist_workers must be a positive integer or 'auto'.")
63
+ if isinstance(xdist_workers, str) and xdist_workers != "auto":
64
+ raise RuntimeError("xdist_workers must be a positive integer or 'auto'.")
65
+ self.xdist_workers = xdist_workers
66
+
67
+ logger.info(
68
+ "Initialized PytestRunner with bound='%s', target='%s', no_tests_score=%s, no_output_score=%s",
69
+ bound,
70
+ target,
71
+ no_tests_score,
72
+ no_output_score,
73
+ )
74
+
75
+ def _is_xdist_installed(self) -> bool:
76
+ try:
77
+ result = subprocess.run(
78
+ self.package_manager.show_command("pytest-xdist"),
79
+ shell=True,
80
+ stdout=subprocess.PIPE,
81
+ stderr=subprocess.PIPE,
82
+ check=False,
83
+ )
84
+ installed = result.returncode == 0
85
+ logger.debug(
86
+ "pytest-xdist availability check returned code %d (installed=%s)",
87
+ result.returncode,
88
+ installed,
89
+ )
90
+ return installed
91
+ except Exception as exc: # pragma: no cover - defensive
92
+ logger.debug("pytest-xdist availability check failed: %s", exc)
93
+ return False
94
+
95
+ def _build_command(self, target: str, use_system_interpreter: bool = False) -> str:
96
+ base_command = f"{sys.executable} -m pytest"
97
+ if self.has_config:
98
+ base_command += f" -c {self.config_path}"
99
+ if self._is_xdist_installed():
100
+ logger.debug("pytest-xdist detected; enabling distributed execution")
101
+ return f"{base_command} -n {self.xdist_workers} {self.target}"
102
+ logger.debug("pytest-xdist not detected; running without distribution")
103
+ return f"{base_command} {self.target}"
104
+
105
+ def _strip_ansi(self, text: str) -> str:
106
+ return self.ANSI_ESCAPE_REGEX.sub("", text)
107
+
108
+ def _is_summary_line(self, line: str) -> bool:
109
+ cleaned = self._strip_ansi(line)
110
+ return bool(
111
+ self.PYTEST_SUMMARY_REGEX.match(cleaned)
112
+ or self.PYTEST_QUIET_SUMMARY_REGEX.match(cleaned)
113
+ )
114
+
115
+ def _install_dependencies(self, base: LayeredCommand) -> None:
116
+ logger.info("Installing pytest dependencies")
117
+ with base:
118
+ base(self.package_manager.install_command("pytest"))
119
+
120
+ def _calculate_score(
121
+ self, ret: int, command_output: List[str], *, verbose: bool = False
122
+ ) -> float:
123
+ logger.info("Calculating pytest score from test results")
124
+
125
+ if len(command_output) == 0:
126
+ logger.info(
127
+ "No pytest output, returning no_output_score: %s", self.no_output_score
128
+ )
129
+ return self.no_output_score
130
+
131
+ # Find the actual summary line by looking for the regex pattern
132
+ # (pytest output may have warnings or other lines after the summary)
133
+ rating_line = None
134
+ for line in reversed(command_output):
135
+ cleaned_line = self._strip_ansi(line)
136
+ if "no tests ran" in cleaned_line.lower():
137
+ logger.info(
138
+ "No tests ran, returning no_tests_score: %s", self.no_tests_score
139
+ )
140
+ return self.no_tests_score
141
+ if self._is_summary_line(line):
142
+ rating_line = cleaned_line
143
+ break
144
+
145
+ if rating_line is None:
146
+ rating_line = self._strip_ansi(command_output[-1])
147
+ if "no tests ran" in rating_line.lower():
148
+ logger.info(
149
+ "No tests ran, returning no_tests_score: %s", self.no_tests_score
150
+ )
151
+ return self.no_tests_score
152
+
153
+ if not (
154
+ self.PYTEST_SUMMARY_REGEX.match(rating_line)
155
+ or self.PYTEST_QUIET_SUMMARY_REGEX.match(rating_line)
156
+ ):
157
+ logger.error("Failed to parse pytest output: %s", rating_line)
158
+ raise ExitEarlyError(
159
+ f"Can't calculate score for pytest on the following line: {rating_line}"
160
+ )
161
+
162
+ failed_match = self.PYTEST_FAILED_REGEX.search(rating_line)
163
+ passed_match = self.PYTEST_PASSED_REGEX.search(rating_line)
164
+ skipped_match = self.PYTEST_SKIPPED_REGEX.search(rating_line)
165
+ failed = int(failed_match.group(1)) if failed_match else 0
166
+ passed = int(passed_match.group(1)) if passed_match else 0
167
+ skipped = int(skipped_match.group(1)) if skipped_match else 0
168
+ assert failed >= 0
169
+ assert passed >= 0
170
+ assert skipped >= 0
171
+
172
+ total_tests = passed + failed + skipped
173
+ if total_tests == 0:
174
+ logger.info(
175
+ "No test results found, returning no_tests_score: %s",
176
+ self.no_tests_score,
177
+ )
178
+ return self.no_tests_score
179
+
180
+ score = passed / total_tests
181
+ logger.info(
182
+ "Pytest score calculated: %.3f (passed: %d, failed: %d, skipped: %d)",
183
+ score,
184
+ passed,
185
+ failed,
186
+ skipped,
187
+ )
188
+ return score
@@ -0,0 +1,138 @@
1
+ import logging
2
+ import os
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Optional, List, Any
6
+ from danielutils import LayeredCommand
7
+
8
+ from ....enforcers import ExitEarlyError
9
+ from ...quality_assurance_runner import QualityAssuranceRunner
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def _removesuffix(string: str, suffix: str) -> str:
15
+ if suffix and string.endswith(suffix):
16
+ return string[: -len(suffix)]
17
+ return string
18
+
19
+
20
+ class UnittestRunner(QualityAssuranceRunner):
21
+ """Quality assurance runner for unittest testing. Scores based on the ratio of passed tests to total tests."""
22
+
23
+ NUM_TESTS_PATTERN: re.Pattern = re.compile(r"Ran (\d+) tests? in \d+\.\d+s")
24
+ NUM_FAILED_PATTERN: re.Pattern = re.compile(
25
+ r"FAILED \((?:failures=(\d+))?(?:, )?(?:errors=(\d+))?(?:, )?(?:skipped=(\d+))?\)|"
26
+ r"FAILED \((?:errors=(\d+))?(?:, )?(?:failures=(\d+))?(?:, )?(?:skipped=(\d+))?\)|"
27
+ r"FAILED \((?:skipped=(\d+))?(?:, )?(?:failures=(\d+))?(?:, )?(?:errors=(\d+))?\)"
28
+ )
29
+
30
+ def _install_dependencies(self, base: LayeredCommand) -> None:
31
+ return None
32
+
33
+ def _pre_command(self) -> None:
34
+ pass
35
+
36
+ def _post_command(self) -> None:
37
+ pass
38
+ # set_current_working_directory(self._cwd)
39
+
40
+ def __init__(
41
+ self,
42
+ target: Optional[str] = "./tests",
43
+ bound: str = ">=0.8",
44
+ no_tests_score: float = 0,
45
+ ) -> None:
46
+ QualityAssuranceRunner.__init__(
47
+ self, name="unittest", bound=bound, target=target
48
+ )
49
+ self.no_tests_score = no_tests_score
50
+ logger.info(
51
+ "Initialized UnittestRunner with target='%s', bound='%s', no_tests_score=%s",
52
+ target,
53
+ bound,
54
+ no_tests_score,
55
+ )
56
+
57
+ def _build_command(
58
+ self, src: str, *args: Any, use_system_interpreter: bool = False
59
+ ) -> str:
60
+ command: str = self.get_executable()
61
+ target = self.target or "./tests"
62
+ rel = _removesuffix(os.path.relpath(src, target), src.lstrip("./\\"))
63
+ command += f" discover -s {rel}"
64
+ normalized_target_path = Path(os.path.join(os.getcwd(), target)).resolve()
65
+ # This is for concurrency reasons
66
+ return f"cd {normalized_target_path} & {command} & cd {Path(os.getcwd()).resolve()}"
67
+
68
+ def _calculate_score(
69
+ self, ret: int, lines: List[str], *, verbose: bool = False
70
+ ) -> float:
71
+ logger.info("Calculating unittest score from test results")
72
+
73
+ try:
74
+ num_tests_ran_line = lines[-3]
75
+ num_tests_failed_line = lines[-1]
76
+ match = self.NUM_TESTS_PATTERN.match(num_tests_ran_line)
77
+ if match is None:
78
+ raise ValueError(
79
+ f"Failed to parse test count from line: {num_tests_ran_line}"
80
+ )
81
+ num_tests = int(match.group(1))
82
+ if num_tests == 0:
83
+ logger.info(
84
+ "No tests found, returning no_tests_score: %s", self.no_tests_score
85
+ )
86
+ return self.no_tests_score
87
+
88
+ num_failed = 0
89
+ num_errors = 0
90
+ if num_tests_failed_line != "OK":
91
+ if num_tests_failed_line.startswith("FAILED"):
92
+ m = self.NUM_FAILED_PATTERN.match(num_tests_failed_line)
93
+ if m:
94
+ # Handle different group positions based on the pattern that matched
95
+ groups = m.groups()
96
+ if groups[0] is not None: # failures= first pattern
97
+ num_failed = int(groups[0] or "0")
98
+ num_errors = int(groups[1] or "0")
99
+ elif groups[3] is not None: # errors= first pattern
100
+ num_failed = int(groups[4] or "0")
101
+ num_errors = int(groups[3] or "0")
102
+ elif groups[6] is not None: # skipped= first pattern
103
+ num_failed = int(groups[7] or "0")
104
+ num_errors = int(groups[8] or "0")
105
+ else:
106
+ # Fallback to original logic if no groups match
107
+ num_failed = int(m.group(1) or "0")
108
+ num_errors = int(m.group(2) or "0")
109
+ else:
110
+ # If regex doesn't match, treat as malformed
111
+ raise ValueError(
112
+ f"Failed to parse FAILED line: {num_tests_failed_line}"
113
+ )
114
+ elif num_tests_failed_line.startswith("OK"):
115
+ # 'OK (skipped=3)' could it also be other stuff?
116
+ pass
117
+
118
+ score = 1 - ((num_failed + num_errors) / num_tests)
119
+ logger.info(
120
+ "Unittest score calculated: %.3f (tests: %d, failed: %d, errors: %d)",
121
+ score,
122
+ num_tests,
123
+ num_failed,
124
+ num_errors,
125
+ )
126
+ return score
127
+
128
+ except Exception as e:
129
+ logger.error("Failed to calculate unittest score: %s", e)
130
+ raise ExitEarlyError(
131
+ f"Failed running Unittest, got exit code {ret}. "
132
+ f"try running manually using: {self._build_command('TARGET')}"
133
+ ) from e
134
+
135
+
136
+ __all__ = [
137
+ "UnittestRunner",
138
+ ]
@@ -0,0 +1,2 @@
1
+ from .github_upload_target import *
2
+ from .pypirc_upload_target import *
@@ -0,0 +1,61 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ from ...upload_target import UploadTarget
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class GithubUploadTarget(UploadTarget):
10
+ """Upload target implementation for GitHub releases. Commits and pushes changes to the git repository."""
11
+
12
+ def upload(self, name: str, version: str, **kwargs: Any) -> None: # type: ignore[override]
13
+ from quickpub.proxy import cm
14
+ from quickpub.enforcers import exit_if
15
+
16
+ logger.info("Starting GitHub upload for version '%s'", version)
17
+
18
+ if self.verbose:
19
+ logger.debug("Staging files for Git commit")
20
+
21
+ ret, stdout, stderr = cm("git add .")
22
+ if ret != 0:
23
+ logger.error(
24
+ "Git add failed with return code %d: %s",
25
+ ret,
26
+ stderr.decode(encoding="utf8"),
27
+ )
28
+ exit_if(ret != 0, stderr.decode(encoding="utf8"))
29
+
30
+ if self.verbose:
31
+ logger.debug(
32
+ "Committing changes with message 'updated to version %s'", version
33
+ )
34
+
35
+ ret, stdout, stderr = cm(f'git commit -m "updated to version {version}"')
36
+ if ret != 0:
37
+ logger.error(
38
+ "Git commit failed with return code %d: %s",
39
+ ret,
40
+ stderr.decode(encoding="utf8"),
41
+ )
42
+ exit_if(ret != 0, stderr.decode(encoding="utf8"))
43
+
44
+ if self.verbose:
45
+ logger.debug("Pushing changes to GitHub")
46
+
47
+ ret, stdout, stderr = cm("git push")
48
+ if ret != 0:
49
+ logger.error(
50
+ "Git push failed with return code %d: %s",
51
+ ret,
52
+ stderr.decode(encoding="utf8"),
53
+ )
54
+ exit_if(ret != 0, stderr.decode(encoding="utf8"))
55
+
56
+ logger.info("Successfully uploaded version '%s' to GitHub", version)
57
+
58
+
59
+ __all__ = [
60
+ "GithubUploadTarget",
61
+ ]
@@ -0,0 +1,90 @@
1
+ import logging
2
+ import re
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from danielutils import file_exists
7
+
8
+ from ....dist_files import find_sdist
9
+ from ..constraint_enforcers import PypircEnforcer
10
+ from ...upload_target import UploadTarget
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class PypircUploadTarget(UploadTarget):
16
+ """Upload target implementation using .pypirc configuration. Uploads packages to PyPI using twine."""
17
+
18
+ REGEX_PATTERN: re.Pattern = PypircEnforcer.PYPIRC_REGEX
19
+
20
+ def upload(self, name: str, version: str, **kwargs: Any) -> None: # type: ignore[override]
21
+ from quickpub.proxy import cm
22
+ from quickpub.enforcers import exit_if
23
+
24
+ logger.info("Starting PyPI upload for package '%s' version '%s'", name, version)
25
+
26
+ self._validate_file_exists()
27
+ self._validate_file_contents()
28
+
29
+ if self.verbose:
30
+ logger.info("Uploading package to PyPI")
31
+
32
+ sdist_path = find_sdist(Path("dist"), name, version)
33
+ ret, stdout, stderr = cm(
34
+ "twine",
35
+ "upload",
36
+ "--config-file",
37
+ ".pypirc",
38
+ sdist_path.as_posix(),
39
+ )
40
+
41
+ if ret != 0:
42
+ logger.error("PyPI upload failed with return code %d: %s", ret, stderr)
43
+ exit_if(
44
+ ret != 0,
45
+ f"Failed uploading the package to pypi. Try running the following command manually:\n"
46
+ f"\ttwine upload --config-file .pypirc {sdist_path.as_posix()}",
47
+ )
48
+
49
+ logger.info(
50
+ "Successfully uploaded package '%s' version '%s' to PyPI", name, version
51
+ )
52
+
53
+ def _validate_file_exists(self) -> None:
54
+ logger.debug("Validating .pypirc file exists at '%s'", self.pypirc_file_path)
55
+ if not file_exists(self.pypirc_file_path):
56
+ logger.error(".pypirc file not found at '%s'", self.pypirc_file_path)
57
+ raise RuntimeError(
58
+ f"{self.__class__.__name__} can't find pypirc file at '{self.pypirc_file_path}'"
59
+ )
60
+ logger.debug(".pypirc file found at '%s'", self.pypirc_file_path)
61
+
62
+ def _validate_file_contents(self) -> None:
63
+ logger.debug("Validating .pypirc file contents at '%s'", self.pypirc_file_path)
64
+ with open(self.pypirc_file_path, "r", encoding="utf8") as f:
65
+ text = f.read()
66
+ if not self.REGEX_PATTERN.match(text):
67
+ logger.error(
68
+ ".pypirc file contents validation failed for '%s'",
69
+ self.pypirc_file_path,
70
+ )
71
+ raise RuntimeError(
72
+ f"{self.__class__.__name__} checked the contents of '{self.pypirc_file_path}' and it failed to match the following regex: r\"{self.REGEX_PATTERN.pattern}\""
73
+ )
74
+ logger.debug(
75
+ ".pypirc file contents validation passed for '%s'", self.pypirc_file_path
76
+ )
77
+
78
+ def __init__(
79
+ self, pypirc_file_path: str = "./.pypirc", verbose: bool = False
80
+ ) -> None:
81
+ super().__init__(verbose)
82
+ self.pypirc_file_path = pypirc_file_path
83
+ logger.info(
84
+ "Initialized PypircUploadTarget with pypirc_file_path='%s', verbose=%s",
85
+ pypirc_file_path,
86
+ verbose,
87
+ )
88
+
89
+
90
+ __all__ = ["PypircUploadTarget"]
@@ -0,0 +1,54 @@
1
+ import logging
2
+ from abc import abstractmethod
3
+ from typing import Dict, Union
4
+
5
+ from danielutils.async_.async_layered_command import AsyncLayeredCommand
6
+
7
+ from .quickpub_strategy import QuickpubStrategy
8
+ from ..enforcers import exit_if
9
+ from ..structures import Dependency
10
+ from .implementations.package_managers.installed_package_parser import (
11
+ parse_installed_packages_output,
12
+ )
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class PackageManager(QuickpubStrategy):
18
+ async def prepare(self, executor: AsyncLayeredCommand) -> None:
19
+ return None
20
+
21
+ @abstractmethod
22
+ async def list_installed(
23
+ self, executor: AsyncLayeredCommand, env_name: str
24
+ ) -> Dict[str, Union[str, Dependency]]: ...
25
+
26
+ @abstractmethod
27
+ def install_command(self, package: str) -> str: ...
28
+
29
+ @abstractmethod
30
+ def show_command(self, package: str) -> str: ...
31
+
32
+ @abstractmethod
33
+ def wrap_command(self, command: str) -> str: ...
34
+
35
+ async def _list_installed_from_command(
36
+ self,
37
+ executor: AsyncLayeredCommand,
38
+ env_name: str,
39
+ list_command: str,
40
+ ) -> Dict[str, Union[str, Dependency]]:
41
+ logger.debug(
42
+ "Executing '%s' on environment '%s'", list_command, env_name
43
+ )
44
+ code, out, _ = await executor(list_command)
45
+ exit_if(
46
+ code != 0,
47
+ f"Failed executing '{list_command}' at env '{env_name}'",
48
+ )
49
+ currently_installed = parse_installed_packages_output(out)
50
+ logger.debug("Found %d installed packages", len(currently_installed))
51
+ return currently_installed
52
+
53
+
54
+ __all__ = ["PackageManager"]