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
quickpub/files.py ADDED
@@ -0,0 +1,246 @@
1
+ import logging
2
+ import re
3
+ from pathlib import Path
4
+ from typing import List, Optional, Dict, Callable
5
+ from danielutils import get_files, file_exists
6
+
7
+ from .classifiers import Classifier
8
+ from .enforcers import ExitEarlyError
9
+ from .structures import Version, Dependency
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def _format_classifiers_string(classifiers: List[Classifier]) -> str:
15
+ classifiers_string = ",\n\t".join([f'"{str(c)}"' for c in classifiers])
16
+ if len(classifiers_string) > 0:
17
+ classifiers_string = f"\n\t{classifiers_string}\n"
18
+ return classifiers_string
19
+
20
+
21
+ def _build_py_typed_section(name: str, src_folder_path: str) -> str:
22
+ for file in get_files(src_folder_path):
23
+ if file == "py.typed":
24
+ logger.debug("Found py.typed file, adding package-data configuration")
25
+ return f"""[tool.setuptools.package-data]
26
+ "{name}" = ["py.typed"]"""
27
+ return ""
28
+
29
+
30
+ def _build_scripts_section(name: str, scripts: Dict[str, Callable]) -> str:
31
+ logger.debug("Adding [project.scripts] section with %d entries", len(scripts))
32
+ scripts_entries = []
33
+ for script_name, func in scripts.items():
34
+ module = func.__module__
35
+ func_name = func.__name__
36
+ if module == "__main__":
37
+ module = f"{name}.__main__"
38
+ entry_point = f"{module}:{func_name}"
39
+ scripts_entries.append(f' {script_name} = "{entry_point}"')
40
+ return "\n[project.scripts]\n" + "\n".join(scripts_entries) + "\n"
41
+
42
+
43
+ def _build_toml_content(
44
+ name: str,
45
+ version: Version,
46
+ author: str,
47
+ author_email: str,
48
+ description: str,
49
+ readme_file_path: str,
50
+ license_file_path: str,
51
+ keywords: List[str],
52
+ min_python: Version,
53
+ dependencies: List[Dependency],
54
+ classifiers_string: str,
55
+ scripts_section: str,
56
+ py_typed: str,
57
+ homepage: str,
58
+ ) -> str:
59
+ normalized_readme_path = readme_file_path.replace("\\", "/")
60
+ normalized_license_path = license_file_path.replace("\\", "/")
61
+ return f"""[build-system]
62
+ requires = ["setuptools>=61.0"]
63
+ build-backend = "setuptools.build_meta"
64
+
65
+ [project]
66
+ name = "{name}"
67
+ version = "{version}"
68
+ authors = [
69
+ {{ name = "{author}", email = "{author_email}" }},
70
+ ]
71
+ dependencies = {[str(dep) for dep in dependencies]}
72
+ keywords = {keywords}
73
+ license = {{ "file" = "{normalized_license_path}" }}
74
+ description = "{description}"
75
+ readme = {{file = "{normalized_readme_path}", content-type = "text/markdown"}}
76
+ requires-python = ">={min_python}"
77
+ classifiers = [{classifiers_string}]{scripts_section}
78
+ [tool.setuptools]
79
+ packages = ["{name}"]
80
+
81
+ {py_typed}
82
+
83
+ [project.urls]
84
+ "Homepage" = "{homepage}"
85
+ "Bug Tracker" = "{homepage}/issues"
86
+ """
87
+
88
+
89
+ def create_toml(
90
+ *,
91
+ name: str,
92
+ src_folder_path: str,
93
+ readme_file_path: str,
94
+ license_file_path: str,
95
+ version: Version,
96
+ author: str,
97
+ author_email: str,
98
+ description: str,
99
+ homepage: str,
100
+ keywords: List[str],
101
+ min_python: Version,
102
+ dependencies: List[Dependency],
103
+ classifiers: List[Classifier],
104
+ scripts: Optional[Dict[str, Callable]] = None,
105
+ ) -> None:
106
+ logger.info("Creating pyproject.toml for package '%s' version '%s'", name, version)
107
+ classifiers_string = _format_classifiers_string(classifiers)
108
+ py_typed = _build_py_typed_section(name, src_folder_path)
109
+ scripts_section = ""
110
+ if scripts:
111
+ scripts_section = _build_scripts_section(name, scripts)
112
+ toml_content = _build_toml_content(
113
+ name,
114
+ version,
115
+ author,
116
+ author_email,
117
+ description,
118
+ readme_file_path,
119
+ license_file_path,
120
+ keywords,
121
+ min_python,
122
+ dependencies,
123
+ classifiers_string,
124
+ scripts_section,
125
+ py_typed,
126
+ homepage,
127
+ )
128
+ with open("pyproject.toml", "w", encoding="utf8") as f:
129
+ f.write(toml_content)
130
+ logger.info("Successfully created pyproject.toml")
131
+
132
+
133
+ def create_setup() -> None:
134
+ logger.info("Creating setup.py file")
135
+ with open("./setup.py", "w", encoding="utf8") as f:
136
+ f.write("from setuptools import setup\n\nsetup()\n")
137
+ logger.info("Successfully created setup.py")
138
+
139
+
140
+ def create_manifest(*, name: str) -> None:
141
+ logger.info("Creating MANIFEST.in for package '%s'", name)
142
+ with open("./MANIFEST.in", "w", encoding="utf8") as f:
143
+ f.write(f"recursive-include {name} *.py")
144
+ logger.info("Successfully created MANIFEST.in")
145
+
146
+
147
+ _PROJECT_SECTION_HEADER = re.compile(r"^\[project]\s*$", re.MULTILINE)
148
+ _NEXT_SECTION = re.compile(r"^\[", re.MULTILINE)
149
+ _PROJECT_VERSION_LINE = re.compile(
150
+ r"^(\s*version\s*=\s*)([\"'])([^\"']*)\2",
151
+ re.MULTILINE,
152
+ )
153
+
154
+
155
+ def update_pyproject_version(
156
+ version: Version,
157
+ pyproject_path: str = "./pyproject.toml",
158
+ ) -> None:
159
+ path = Path(pyproject_path)
160
+ if not path.exists():
161
+ raise ExitEarlyError(f"pyproject.toml not found at '{path}'")
162
+
163
+ content = path.read_text(encoding="utf8")
164
+ header_match = _PROJECT_SECTION_HEADER.search(content)
165
+ if header_match is None:
166
+ raise ExitEarlyError(f"No [project] section in '{path}'")
167
+
168
+ section_start = header_match.end()
169
+ next_section = _NEXT_SECTION.search(content, section_start)
170
+ section_end = next_section.start() if next_section else len(content)
171
+ project_section = content[section_start:section_end]
172
+ version_match = _PROJECT_VERSION_LINE.search(project_section)
173
+ if version_match is None:
174
+ raise ExitEarlyError(f"No project.version in '{path}'")
175
+
176
+ quote = version_match.group(2)
177
+ updated_section = (
178
+ project_section[: version_match.start()]
179
+ + f"{version_match.group(1)}{quote}{version}{quote}"
180
+ + project_section[version_match.end() :]
181
+ )
182
+ path.write_text(
183
+ content[:section_start] + updated_section + content[section_end:],
184
+ encoding="utf8",
185
+ )
186
+ logger.info("Updated [project].version in '%s' to '%s'", path, version)
187
+
188
+
189
+ def add_version_to_init(src_folder_path: str, version: Version) -> None:
190
+ src_path = Path(src_folder_path).resolve()
191
+ init_file_path = src_path / "__init__.py"
192
+
193
+ if not init_file_path.exists():
194
+ logger.warning("__init__.py not found at '%s', creating it", init_file_path)
195
+ init_file_path.parent.mkdir(parents=True, exist_ok=True)
196
+ with open(init_file_path, "w", encoding="utf8") as f:
197
+ f.write(f'__version__ = "{version}"\n')
198
+ logger.info("Created __init__.py with __version__ = '%s'", version)
199
+ return
200
+
201
+ with open(init_file_path, "r", encoding="utf8") as f:
202
+ content = f.read()
203
+
204
+ version_line = f'__version__ = "{version}"'
205
+ version_pattern = r'__version__\s*=\s*["\'].*?["\']'
206
+
207
+ if re.search(version_pattern, content):
208
+ new_content = re.sub(version_pattern, version_line, content)
209
+ logger.info("Updated existing __version__ in __init__.py to '%s'", version)
210
+ else:
211
+ lines = content.splitlines()
212
+ if not lines:
213
+ new_content = f"{version_line}\n"
214
+ else:
215
+ import_end_index = 0
216
+ for i, line in enumerate(lines):
217
+ stripped = line.strip()
218
+ if not stripped or stripped.startswith("#"):
219
+ continue
220
+ if stripped.startswith("from") or stripped.startswith("import"):
221
+ import_end_index = i + 1
222
+ else:
223
+ break
224
+
225
+ if import_end_index < len(lines):
226
+ lines.insert(import_end_index, version_line)
227
+ else:
228
+ lines.append(version_line)
229
+
230
+ new_content = "\n".join(lines)
231
+ if not content.endswith("\n"):
232
+ new_content += "\n"
233
+
234
+ logger.info("Added __version__ = '%s' to __init__.py", version)
235
+
236
+ with open(init_file_path, "w", encoding="utf8") as f:
237
+ f.write(new_content)
238
+
239
+
240
+ __all__ = [
241
+ "create_setup",
242
+ "create_toml",
243
+ "create_manifest",
244
+ "add_version_to_init",
245
+ "update_pyproject_version",
246
+ ]
quickpub/logging_.py ADDED
@@ -0,0 +1,74 @@
1
+ import logging
2
+ import sys
3
+ from typing import Optional
4
+
5
+ # Global log level that can be modified by users
6
+ _LOG_LEVEL = logging.INFO
7
+
8
+
9
+ class QuickpubLogFilter(logging.Filter):
10
+
11
+ def filter(self, record: logging.LogRecord) -> bool:
12
+ return record.name.startswith("quickpub")
13
+
14
+
15
+ class TqdmLoggingHandler(logging.Handler):
16
+
17
+ def __init__(self, level: int = logging.NOTSET):
18
+ super().__init__(level)
19
+ self._tqdm = None
20
+
21
+ def emit(self, record: logging.LogRecord) -> None:
22
+ try:
23
+ import tqdm
24
+
25
+ msg = self.format(record)
26
+ tqdm.tqdm.write(msg, file=sys.stdout)
27
+ except ImportError:
28
+ # Fallback if tqdm becomes unavailable
29
+ print(self.format(record), file=sys.stdout)
30
+ except Exception:
31
+ self.handleError(record)
32
+
33
+
34
+ def setup_logging(level: Optional[int] = None) -> None:
35
+ global _LOG_LEVEL
36
+
37
+ if level is not None:
38
+ _LOG_LEVEL = level
39
+
40
+ logger = logging.getLogger()
41
+ logger.setLevel(_LOG_LEVEL)
42
+
43
+ logger.handlers.clear()
44
+
45
+ formatter = logging.Formatter(
46
+ "[quickpub] %(levelname)-5s %(asctime)s %(filename)s:%(lineno)d | %(message)s"
47
+ )
48
+
49
+ try:
50
+ import tqdm
51
+
52
+ handler: logging.Handler = TqdmLoggingHandler()
53
+ except ImportError:
54
+ handler = logging.StreamHandler(sys.stdout)
55
+ handler.setLevel(_LOG_LEVEL)
56
+ handler.setFormatter(formatter)
57
+
58
+ handler.addFilter(QuickpubLogFilter())
59
+
60
+ logger.addHandler(handler)
61
+
62
+
63
+ def set_log_level(level: int) -> None:
64
+ global _LOG_LEVEL
65
+ _LOG_LEVEL = level
66
+
67
+ logger = logging.getLogger()
68
+ logger.setLevel(level)
69
+
70
+ for handler in logger.handlers:
71
+ handler.setLevel(level)
72
+
73
+
74
+ __all__ = ["setup_logging", "set_log_level", "QuickpubLogFilter"]
@@ -0,0 +1,43 @@
1
+ from pathlib import Path
2
+ from typing import Optional, Tuple
3
+
4
+ from .strategies.implementations.package_managers.pip_package_manager import (
5
+ PipPackageManager,
6
+ )
7
+ from .strategies.implementations.package_managers.uv_package_manager import (
8
+ UvPackageManager,
9
+ )
10
+ from .strategies.implementations.python_providers.default_python_provider import (
11
+ DefaultPythonProvider,
12
+ )
13
+ from .strategies.implementations.python_providers.uv_python_provider import (
14
+ UvPythonProvider,
15
+ )
16
+ from .strategies.package_manager import PackageManager
17
+ from .strategies.python_provider import PythonProvider
18
+
19
+
20
+ def detect_package_manager(project_root: Path = Path(".")) -> PackageManager:
21
+ if (project_root / "uv.lock").exists():
22
+ return UvPackageManager(project_root=project_root)
23
+ return PipPackageManager()
24
+
25
+
26
+ def resolve_publish_environment(
27
+ project_root: Path = Path("."),
28
+ python_interpreter_provider: Optional[PythonProvider] = None,
29
+ package_manager: Optional[PackageManager] = None,
30
+ ) -> Tuple[PackageManager, PythonProvider]:
31
+ if package_manager is None:
32
+ package_manager = detect_package_manager(project_root)
33
+
34
+ if python_interpreter_provider is None:
35
+ if isinstance(package_manager, UvPackageManager):
36
+ python_interpreter_provider = UvPythonProvider(project_root=project_root)
37
+ else:
38
+ python_interpreter_provider = DefaultPythonProvider()
39
+
40
+ return package_manager, python_interpreter_provider
41
+
42
+
43
+ __all__ = ["detect_package_manager", "resolve_publish_environment"]
quickpub/proxy.py ADDED
@@ -0,0 +1,35 @@
1
+ import logging
2
+ import os
3
+ from typing import Tuple, Any
4
+ import requests
5
+ import danielutils
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ def cm(*args: Any, **kwargs: Any) -> Tuple[int, bytes, bytes]:
11
+ logger.debug("Executing command: %s", " ".join(args))
12
+ result = danielutils.cm(*args, **kwargs)
13
+ logger.debug("Command completed with return code: %d", result[0])
14
+ return result
15
+
16
+
17
+ def os_system(command: str) -> int:
18
+ logger.debug("Executing system command: %s", command)
19
+ result = os.system(command)
20
+ logger.debug("System command completed with return code: %d", result)
21
+ return result
22
+
23
+
24
+ def get(*args: Any, **kwargs: Any) -> requests.models.Response:
25
+ logger.debug(
26
+ "Making HTTP GET request to: %s", args[0] if args else "URL not provided"
27
+ )
28
+ response = requests.get(*args, **kwargs)
29
+ logger.debug(
30
+ "HTTP GET request completed with status code: %d", response.status_code
31
+ )
32
+ return response
33
+
34
+
35
+ __all__ = ["cm", "os_system", "get"]
quickpub/py.typed ADDED
File without changes