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
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
|