pkg-deploy 1.0.0__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.
- pkg_deploy/__init__.py +3 -0
- pkg_deploy/build.py +179 -0
- pkg_deploy/deploy.py +276 -0
- pkg_deploy/upload.py +70 -0
- pkg_deploy/utils.py +238 -0
- pkg_deploy/version_managment.py +143 -0
- pkg_deploy-1.0.0.dist-info/METADATA +15 -0
- pkg_deploy-1.0.0.dist-info/RECORD +10 -0
- pkg_deploy-1.0.0.dist-info/WHEEL +5 -0
- pkg_deploy-1.0.0.dist-info/top_level.txt +1 -0
pkg_deploy/__init__.py
ADDED
pkg_deploy/build.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import copy
|
|
4
|
+
import logging
|
|
5
|
+
import textwrap
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional, Dict
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from tomlkit.toml_document import TOMLDocument
|
|
12
|
+
|
|
13
|
+
from .utils import save_config, is_uv_venv, ensure_uv_installed
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class DeployConfig:
|
|
21
|
+
package_name: str
|
|
22
|
+
project_dir: Path
|
|
23
|
+
pyproject_path: Path
|
|
24
|
+
version_type: str
|
|
25
|
+
new_version: str
|
|
26
|
+
use_cython: bool
|
|
27
|
+
is_uv_venv: bool
|
|
28
|
+
repository_name: str
|
|
29
|
+
repository_url: Optional[str] = None
|
|
30
|
+
username: Optional[str] = None
|
|
31
|
+
password: Optional[str] = None
|
|
32
|
+
dry_run: bool = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BuildStrategy(ABC):
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def build_cmd():
|
|
39
|
+
if is_uv_venv():
|
|
40
|
+
ensure_uv_installed()
|
|
41
|
+
cmd = ["uv", "build", "--wheel"]
|
|
42
|
+
else:
|
|
43
|
+
cmd = [sys.executable, "-m", "build", "--wheel"]
|
|
44
|
+
return cmd
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def build(self, config: DeployConfig, toml_config: TOMLDocument) -> bool:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class StandardBuildStrategy(BuildStrategy):
|
|
52
|
+
|
|
53
|
+
def build(self, config: DeployConfig, toml_config: TOMLDocument) -> bool:
|
|
54
|
+
cmd = self.build_cmd()
|
|
55
|
+
logger.info(f"Running: {' '.join(cmd)}")
|
|
56
|
+
result = subprocess.run(cmd, capture_output=True, text=True, cwd=config.project_dir)
|
|
57
|
+
if result.returncode != 0:
|
|
58
|
+
raise ValueError(f"Build failed, \nstdout: {result.stdout}\nstderr: {result.stderr}")
|
|
59
|
+
logger.info("Standard build completed successfully")
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
class CythonBuildStrategy(BuildStrategy):
|
|
63
|
+
|
|
64
|
+
def build(self, config: DeployConfig, toml_config: TOMLDocument) -> bool:
|
|
65
|
+
try:
|
|
66
|
+
self.prepare_pyproject_for_cython_build(config.project_dir, toml_config)
|
|
67
|
+
self.create_setup_py_for_cython(config.project_dir, toml_config)
|
|
68
|
+
cmd = self.build_cmd()
|
|
69
|
+
logger.info(f"Running Cython build: {' '.join(cmd)}")
|
|
70
|
+
env = os.environ.copy()
|
|
71
|
+
env['CYTHONIZE'] = '1'
|
|
72
|
+
result = subprocess.run(cmd, capture_output=True, text=True, env=env, cwd=config.project_dir)
|
|
73
|
+
if result.returncode != 0:
|
|
74
|
+
raise ValueError(f"Cython build failed, \nstdout: {result.stdout}\nstderr: {result.stderr}")
|
|
75
|
+
logger.info("Cython build completed successfully")
|
|
76
|
+
return True
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.error(f"Cython build error: {e}")
|
|
79
|
+
return False
|
|
80
|
+
finally:
|
|
81
|
+
self.restore_pyproject_toml(project_dir=config.project_dir, original_toml_config=toml_config)
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def prepare_pyproject_for_cython_build(project_dir: Path, toml_config: TOMLDocument):
|
|
85
|
+
pyproject_path = project_dir / "pyproject.toml"
|
|
86
|
+
if pyproject_path.exists():
|
|
87
|
+
new_config = copy.deepcopy(toml_config)
|
|
88
|
+
|
|
89
|
+
# Add Cython build dependency
|
|
90
|
+
if 'build-system' not in new_config:
|
|
91
|
+
new_config['build-system'] = {}
|
|
92
|
+
|
|
93
|
+
if 'requires' not in new_config['build-system']:
|
|
94
|
+
new_config['build-system']['requires'] = []
|
|
95
|
+
|
|
96
|
+
cython_deps = ['setuptools', 'Cython']
|
|
97
|
+
requires = new_config['build-system']['requires']
|
|
98
|
+
for dep in cython_deps:
|
|
99
|
+
if not any(req.startswith(dep) for req in requires):
|
|
100
|
+
new_config['build-system']['requires'].append(dep)
|
|
101
|
+
|
|
102
|
+
if 'build-backend' not in new_config['build-system'] or new_config['build-system'][
|
|
103
|
+
'build-backend'] != 'setuptools.build_meta':
|
|
104
|
+
new_config['build-system']['build-backend'] = 'setuptools.build_meta'
|
|
105
|
+
save_config(new_config, pyproject_path)
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def restore_pyproject_toml(project_dir: Path, original_toml_config: TOMLDocument):
|
|
109
|
+
pyproject_path = project_dir / "pyproject.toml"
|
|
110
|
+
if original_toml_config:
|
|
111
|
+
save_config(original_toml_config, pyproject_path)
|
|
112
|
+
logger.info("Restored original pyproject.toml")
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def create_setup_py_for_cython(project_dir: Path, toml_config: TOMLDocument):
|
|
116
|
+
if "authors" in toml_config["project"]:
|
|
117
|
+
author_names = ", ".join(p["name"] for p in toml_config["project"]["authors"] if "name" in p)
|
|
118
|
+
author_emails = ", ".join(p["email"] for p in toml_config["project"]["authors"] if "email" in p)
|
|
119
|
+
else:
|
|
120
|
+
author_names = ""
|
|
121
|
+
author_emails = ""
|
|
122
|
+
if "scripts" in toml_config["project"]:
|
|
123
|
+
entry_points = [f"{k}={v}" for k, v in toml_config["project"]["scripts"].items()]
|
|
124
|
+
else:
|
|
125
|
+
entry_points = []
|
|
126
|
+
setup_py_content = textwrap.dedent(f'''
|
|
127
|
+
import glob
|
|
128
|
+
from Cython.Build import cythonize
|
|
129
|
+
from setuptools import setup, find_packages
|
|
130
|
+
from setuptools.dist import Distribution
|
|
131
|
+
from setuptools.command.build_py import build_py as _build_py
|
|
132
|
+
|
|
133
|
+
py_files = glob.glob("src/**/**/*.py", recursive=True)
|
|
134
|
+
py_files = [f for f in py_files if not f.endswith("__init__.py")]
|
|
135
|
+
|
|
136
|
+
class build_py(_build_py):
|
|
137
|
+
def find_package_modules(self, package, package_dir):
|
|
138
|
+
modules = super().find_package_modules(package, package_dir)
|
|
139
|
+
if self.distribution.ext_modules:
|
|
140
|
+
# Get the list of compiled module names
|
|
141
|
+
compiled_modules = {{ext.name for ext in self.distribution.ext_modules}}
|
|
142
|
+
# Filter out the modules that are compiled
|
|
143
|
+
modules = [
|
|
144
|
+
(pkg, mod, file) for (pkg, mod, file) in modules
|
|
145
|
+
if f"{{pkg}}.{{mod}}" not in compiled_modules
|
|
146
|
+
]
|
|
147
|
+
return modules
|
|
148
|
+
|
|
149
|
+
class BinaryDistribution(Distribution):
|
|
150
|
+
def has_ext_modules(self):
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
setup(
|
|
154
|
+
name="{toml_config["project"]["name"]}",
|
|
155
|
+
version="{toml_config["project"]["version"]}",
|
|
156
|
+
{f"author='{author_names}'," if author_names else ""}
|
|
157
|
+
{f"author_email='{author_emails}'," if author_emails else ""}
|
|
158
|
+
{f"description='{toml_config["project"]["description"]}'," if toml_config["project"].get("description", "") else ""}
|
|
159
|
+
{f"python_requires='{toml_config["project"]["requires-python"]}'," if toml_config["project"].get("requires-python") else ""}
|
|
160
|
+
{f"install_requires={toml_config["project"]["dependencies"]}," if toml_config["project"].get("dependencies") and len(toml_config["project"]["dependencies"]) > 0 else ""}
|
|
161
|
+
entry_points={{
|
|
162
|
+
'console_scripts': {entry_points}
|
|
163
|
+
}},
|
|
164
|
+
packages=find_packages(where="src"),
|
|
165
|
+
package_dir={{"": "src"}},
|
|
166
|
+
include_package_data=True,
|
|
167
|
+
ext_modules=cythonize(
|
|
168
|
+
py_files,
|
|
169
|
+
compiler_directives={{'language_level': "3"}},
|
|
170
|
+
),
|
|
171
|
+
distclass=BinaryDistribution,
|
|
172
|
+
setup_requires=["cython>=3.1"],
|
|
173
|
+
cmdclass={{'build_py': build_py}},
|
|
174
|
+
zip_safe=False
|
|
175
|
+
)
|
|
176
|
+
''').strip()
|
|
177
|
+
|
|
178
|
+
with open(project_dir / "setup.py", 'w') as f:
|
|
179
|
+
f.write(setup_py_content)
|
pkg_deploy/deploy.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import glob
|
|
4
|
+
import shutil
|
|
5
|
+
import logging
|
|
6
|
+
import argparse
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .upload import Upload, NexusUpload
|
|
11
|
+
from .version_managment import VersionManager
|
|
12
|
+
from .build import DeployConfig, CythonBuildStrategy, StandardBuildStrategy
|
|
13
|
+
from .utils import get_pypirc_info, get_credentials, is_uv_venv, validate_version_arg
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logging.basicConfig(
|
|
17
|
+
level=logging.INFO,
|
|
18
|
+
format="%(asctime)s [%(levelname)-5.5s] [%(name)-30.30s] [%(lineno)-4.4s] [%(processName)-12.12s]: %(message)s"
|
|
19
|
+
)
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_args(args):
|
|
24
|
+
parser = argparse.ArgumentParser(
|
|
25
|
+
description="Modern Python Package Deployment Tool",
|
|
26
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
27
|
+
epilog="""
|
|
28
|
+
Examples:
|
|
29
|
+
# Deploy to PyPI, patch version
|
|
30
|
+
python deploy.py --package-name my-package --version-type patch
|
|
31
|
+
|
|
32
|
+
# Deploy to private Nexus, using cython
|
|
33
|
+
python deploy.py --package-name my-package --version-type minor
|
|
34
|
+
--repository-url https://nexus.example.com/repository/pypi-internal/
|
|
35
|
+
--username admin
|
|
36
|
+
--password secret
|
|
37
|
+
|
|
38
|
+
# Dry run
|
|
39
|
+
python deploy.py --package-name my-package --version-type patch --dry-run
|
|
40
|
+
""")
|
|
41
|
+
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--project-dir",
|
|
44
|
+
type=Path,
|
|
45
|
+
default=Path.cwd(),
|
|
46
|
+
help="Project directory (default: current directory)"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--version-type", "-vt",
|
|
51
|
+
default="patch",
|
|
52
|
+
help="Version bump type (default: patch)"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"--new-version", "-v",
|
|
57
|
+
type=validate_version_arg,
|
|
58
|
+
help="New version number, if not specified, a new version will be resolved by version-type"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--cython", "-c",
|
|
63
|
+
action="store_true",
|
|
64
|
+
help="Use Cython for compilation"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--repository-name", "-rn",
|
|
69
|
+
help="Repository name (.pypirc)"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--repository-url", "-rl",
|
|
74
|
+
help="Repository URL"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--username", "-u",
|
|
79
|
+
help="Username for authentication"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--password", "-p",
|
|
84
|
+
help="Password for authentication"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"--no-git-push",
|
|
89
|
+
action="store_true",
|
|
90
|
+
help="Push local changes to Git repository after build"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
"--interactive", "-i",
|
|
95
|
+
action="store_true",
|
|
96
|
+
help="Force interactive credential input (useful for Nexus)"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
parser.add_argument(
|
|
100
|
+
"--dry-run",
|
|
101
|
+
action="store_true",
|
|
102
|
+
help="Perform a dry run without actual deployment"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
parser.add_argument(
|
|
106
|
+
"--verbose", "-V",
|
|
107
|
+
action="store_true",
|
|
108
|
+
help="Enable verbose logging"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
args = parser.parse_args(args)
|
|
112
|
+
if not args.repository_url and not args.repository_name:
|
|
113
|
+
parser.error("Either --repository-url or --repository-name must be provided.")
|
|
114
|
+
return args
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class PackageDeploy:
|
|
118
|
+
def __init__(self):
|
|
119
|
+
args = sys.argv[1:]
|
|
120
|
+
self.args = parse_args(args)
|
|
121
|
+
if not (self.args.project_dir / "pyproject.toml").exists():
|
|
122
|
+
raise ValueError("pyproject.toml not found")
|
|
123
|
+
|
|
124
|
+
if self.args.verbose:
|
|
125
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
126
|
+
|
|
127
|
+
self.check_require_package(self.args.cython)
|
|
128
|
+
|
|
129
|
+
pypirc_info = get_pypirc_info()
|
|
130
|
+
repos = pypirc_info["repositories"]
|
|
131
|
+
if self.args.repository_name and self.args.repository_name in repos:
|
|
132
|
+
repository_info = repos[self.args.repository_name]
|
|
133
|
+
url = repository_info["repository"]
|
|
134
|
+
username = repository_info["username"]
|
|
135
|
+
password = repository_info["password"]
|
|
136
|
+
elif self.args.repository_name:
|
|
137
|
+
raise ValueError("Repository name is provided but not found in .pypirc")
|
|
138
|
+
else:
|
|
139
|
+
url = self.args.repository_url
|
|
140
|
+
username, password = get_credentials(self.args)
|
|
141
|
+
|
|
142
|
+
pyproject_path = self.args.project_dir / "pyproject.toml"
|
|
143
|
+
|
|
144
|
+
self.version_manager = VersionManager(pyproject_path)
|
|
145
|
+
self.config = DeployConfig(
|
|
146
|
+
package_name=self.version_manager.toml_config["project"]["name"],
|
|
147
|
+
project_dir=self.args.project_dir,
|
|
148
|
+
pyproject_path=pyproject_path,
|
|
149
|
+
version_type=self.args.version_type,
|
|
150
|
+
new_version=self.args.new_version,
|
|
151
|
+
use_cython=self.args.cython,
|
|
152
|
+
is_uv_venv=is_uv_venv(),
|
|
153
|
+
repository_name=self.args.repository_name,
|
|
154
|
+
repository_url=url,
|
|
155
|
+
username=username,
|
|
156
|
+
password=password,
|
|
157
|
+
dry_run=self.args.dry_run
|
|
158
|
+
)
|
|
159
|
+
self.setup_file_exist = (self.config.project_dir / "setup.py").exists()
|
|
160
|
+
|
|
161
|
+
def deploy(self):
|
|
162
|
+
logger.info("=== Deployment Configuration ===")
|
|
163
|
+
for arg_name, arg_value in vars(self.args).items():
|
|
164
|
+
display_value = arg_value
|
|
165
|
+
if arg_name in ('username', 'password') and arg_value is not None:
|
|
166
|
+
display_value = '***MASKED***'
|
|
167
|
+
logger.info(f"{arg_name}: {display_value}")
|
|
168
|
+
logger.info("=================================")
|
|
169
|
+
logger.info(f"Starting deployment")
|
|
170
|
+
try:
|
|
171
|
+
self.check_git_status()
|
|
172
|
+
new_version = self.version_manager.bump_version(version_type=self.config.version_type,
|
|
173
|
+
new_version=self.config.new_version)
|
|
174
|
+
logger.info(f"New version: {new_version}")
|
|
175
|
+
|
|
176
|
+
if self.config.use_cython:
|
|
177
|
+
build_strategy = CythonBuildStrategy()
|
|
178
|
+
else:
|
|
179
|
+
build_strategy = StandardBuildStrategy()
|
|
180
|
+
|
|
181
|
+
uploaded = False
|
|
182
|
+
if build_strategy.build(self.config, self.version_manager.toml_config):
|
|
183
|
+
upload_strategy = self.get_upload_strategy(self.config)
|
|
184
|
+
dist_dir = self.config.project_dir / "dist"
|
|
185
|
+
uploaded = upload_strategy.upload(self.config, dist_dir)
|
|
186
|
+
|
|
187
|
+
self.cleanup_build()
|
|
188
|
+
if uploaded and not self.args.no_git_push:
|
|
189
|
+
self.git_push(new_version=new_version)
|
|
190
|
+
else:
|
|
191
|
+
self.git_roll_back()
|
|
192
|
+
|
|
193
|
+
logger.info('Deploy completed')
|
|
194
|
+
except Exception as e:
|
|
195
|
+
logger.error(f"Deployment failed: {e}", exc_info=True)
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def check_require_package(cython: bool):
|
|
200
|
+
required_packages = ["build", "twine", "toml", "tomlkit"]
|
|
201
|
+
if cython:
|
|
202
|
+
required_packages.append("Cython")
|
|
203
|
+
|
|
204
|
+
missing_packages = []
|
|
205
|
+
for package in required_packages:
|
|
206
|
+
try:
|
|
207
|
+
__import__(package)
|
|
208
|
+
except ImportError:
|
|
209
|
+
missing_packages.append(package)
|
|
210
|
+
|
|
211
|
+
if missing_packages:
|
|
212
|
+
logger.error(f"Missing required packages: {', '.join(missing_packages)}")
|
|
213
|
+
logger.error(f"Install them with: pip install {' '.join(missing_packages)}")
|
|
214
|
+
raise ValueError("Missing required packages")
|
|
215
|
+
|
|
216
|
+
def cleanup_build(self):
|
|
217
|
+
logger.info('Deleting build, dist and egg-info files after deployment')
|
|
218
|
+
shutil.rmtree('dist', ignore_errors=True)
|
|
219
|
+
shutil.rmtree('build', ignore_errors=True)
|
|
220
|
+
shutil.rmtree(f'src/{self.config.package_name}.egg-info', ignore_errors=True)
|
|
221
|
+
egg_info_name = self.config.package_name.replace("-", "_")
|
|
222
|
+
shutil.rmtree(f'src/{egg_info_name}.egg-info', ignore_errors=True)
|
|
223
|
+
launcher_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
|
|
224
|
+
directory = os.path.join(launcher_dir, 'src', self.config.package_name.replace("-", "_"))
|
|
225
|
+
c_files = glob.glob(os.path.join(directory, '**', '*.c'), recursive=True)
|
|
226
|
+
if not self.setup_file_exist:
|
|
227
|
+
Path("setup.py").unlink(missing_ok=True)
|
|
228
|
+
for file_path in c_files:
|
|
229
|
+
Path(file_path).unlink(missing_ok=True)
|
|
230
|
+
|
|
231
|
+
def check_git_status(self):
|
|
232
|
+
logger.info("Checking git status, --porcelain to make sure git repo is clean")
|
|
233
|
+
result = subprocess.run(
|
|
234
|
+
["git", "status", "--porcelain"],
|
|
235
|
+
cwd=self.config.project_dir,
|
|
236
|
+
capture_output=True,
|
|
237
|
+
text=True
|
|
238
|
+
)
|
|
239
|
+
if result.returncode != 0:
|
|
240
|
+
raise IOError(f"Git command failed: {result.stderr.strip()}")
|
|
241
|
+
if result.stdout.strip():
|
|
242
|
+
raise IOError(f"Git repo is NOT clean: \n{result.stdout}")
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def git_push(new_version: str):
|
|
246
|
+
try:
|
|
247
|
+
subprocess.check_output(['git', 'add', '.'], stderr=subprocess.STDOUT)
|
|
248
|
+
subprocess.check_output(['git', 'commit', '-m', f'Bump version to {new_version}'], stderr=subprocess.STDOUT)
|
|
249
|
+
tag_name = f"v{new_version}"
|
|
250
|
+
subprocess.check_output(['git', 'tag', '-a', tag_name, '-m', f'Release {tag_name}'], stderr=subprocess.STDOUT)
|
|
251
|
+
logger.info(f"Created Git tag: {tag_name}")
|
|
252
|
+
subprocess.check_output(['git', 'push', '--follow-tags'], stderr=subprocess.STDOUT)
|
|
253
|
+
logger.info('Pushing to github')
|
|
254
|
+
except subprocess.CalledProcessError as ex:
|
|
255
|
+
logger.error(f"Git command failed: {ex.output.decode()}")
|
|
256
|
+
logger.warning('Failed to push bump version commit. Please push manually.')
|
|
257
|
+
except Exception as ex:
|
|
258
|
+
logger.error(f"Unexpected error: {ex}")
|
|
259
|
+
logger.warning('Failed to push bump version commit. Please push manually.')
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
def git_roll_back():
|
|
263
|
+
try:
|
|
264
|
+
subprocess.check_output(['git', 'restore', '.'], stderr=subprocess.STDOUT)
|
|
265
|
+
logger.info('Restored changes')
|
|
266
|
+
except subprocess.CalledProcessError as ex:
|
|
267
|
+
logger.error(f"Git command failed: {ex.output.decode()}")
|
|
268
|
+
except Exception as ex:
|
|
269
|
+
logger.error(f"Unexpected error: {ex}")
|
|
270
|
+
logger.warning('Failed to roll back changes. Please roll back manually.')
|
|
271
|
+
|
|
272
|
+
@staticmethod
|
|
273
|
+
def get_upload_strategy(config) -> Upload:
|
|
274
|
+
return NexusUpload()
|
|
275
|
+
|
|
276
|
+
|
pkg_deploy/upload.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import sys
|
|
3
|
+
import logging
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
|
|
8
|
+
from .build import DeployConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Upload(ABC):
|
|
15
|
+
"""Deploy Base class"""
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def upload(self, config: DeployConfig, dist_dir: Path) -> bool:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NexusUpload(Upload):
|
|
23
|
+
"""Nexus Deploy"""
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def get_wheel_files(config: DeployConfig):
|
|
27
|
+
wheel_files = []
|
|
28
|
+
for binary in (config.project_dir / 'dist').iterdir():
|
|
29
|
+
if config.package_name.replace("-", "_") in binary.name and binary.suffix == '.whl':
|
|
30
|
+
wheel_files.append(binary.name)
|
|
31
|
+
if len(wheel_files) != 1:
|
|
32
|
+
raise ValueError(f"Unable to determine wheel, candidates are: {wheel_files}")
|
|
33
|
+
wheel_file = wheel_files[0]
|
|
34
|
+
logger.info(f"Built {wheel_file}")
|
|
35
|
+
return wheel_file
|
|
36
|
+
|
|
37
|
+
def upload(self, config: DeployConfig, dist_dir: Path) -> bool:
|
|
38
|
+
try:
|
|
39
|
+
if not config.repository_url:
|
|
40
|
+
raise ValueError("Repository URL is required for Nexus deployment")
|
|
41
|
+
|
|
42
|
+
wheel_file = self.get_wheel_files(config)
|
|
43
|
+
|
|
44
|
+
if config.dry_run:
|
|
45
|
+
cmd = [sys.executable, "-m", "twine", "check",
|
|
46
|
+
f"dist/{wheel_file}"
|
|
47
|
+
]
|
|
48
|
+
else:
|
|
49
|
+
cmd = [sys.executable, "-m", "twine", "upload",
|
|
50
|
+
"--repository-url", config.repository_url,
|
|
51
|
+
f"dist/{wheel_file}",
|
|
52
|
+
"--disable-progress-bar"
|
|
53
|
+
]
|
|
54
|
+
if config.username:
|
|
55
|
+
cmd.extend(["--username", config.username])
|
|
56
|
+
if config.password:
|
|
57
|
+
cmd.extend(["--password", config.password])
|
|
58
|
+
|
|
59
|
+
logger.info(f"Running: {' '.join(cmd)}")
|
|
60
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
61
|
+
|
|
62
|
+
if result.returncode != 0:
|
|
63
|
+
raise ValueError(f"Nexus build failed, \nstdout: {result.stdout}\nstderr: {result.stderr}")
|
|
64
|
+
|
|
65
|
+
logger.info("Package deployed to Nexus successfully")
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.error(f"Nexus deploy error: {e}")
|
|
70
|
+
return False
|
pkg_deploy/utils.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import sys
|
|
4
|
+
import shutil
|
|
5
|
+
import tomlkit
|
|
6
|
+
import logging
|
|
7
|
+
import getpass
|
|
8
|
+
import argparse
|
|
9
|
+
import subprocess
|
|
10
|
+
import configparser
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
from tomlkit.toml_document import TOMLDocument
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_uv_venv() -> bool:
|
|
20
|
+
"""Detect if the current virtual environment was created by uv (supports versioned marker)."""
|
|
21
|
+
if not hasattr(sys, 'prefix') or not sys.prefix:
|
|
22
|
+
return False
|
|
23
|
+
pyvenv_cfg = Path(sys.prefix) / "pyvenv.cfg"
|
|
24
|
+
if not pyvenv_cfg.exists():
|
|
25
|
+
return False
|
|
26
|
+
try:
|
|
27
|
+
with open(pyvenv_cfg, "r", encoding="utf-8") as f:
|
|
28
|
+
for line in f:
|
|
29
|
+
line = line.strip()
|
|
30
|
+
if line.lower().startswith("uv ="):
|
|
31
|
+
logger.info("Detected uv-managed Python.")
|
|
32
|
+
return True
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
def setup_uv_compatibility():
|
|
38
|
+
if is_uv_venv():
|
|
39
|
+
logger.info("Setting PIP_USE_VIRTUALENV=1 for build compatibility.")
|
|
40
|
+
os.environ["PIP_USE_VIRTUALENV"] = "1"
|
|
41
|
+
return True
|
|
42
|
+
else:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_credentials(args) -> tuple[Optional[str], Optional[str]]:
|
|
47
|
+
username = args.username
|
|
48
|
+
password = args.password
|
|
49
|
+
is_pypi = args.repository_name and "pypi" in args.repository_name.lower()
|
|
50
|
+
force_interactive = args.interactive
|
|
51
|
+
|
|
52
|
+
if is_pypi:
|
|
53
|
+
logger.info(" Detected PyPI repository - API tokens are recommended over passwords")
|
|
54
|
+
|
|
55
|
+
if not args.dry_run and (force_interactive or (not username and not password)):
|
|
56
|
+
logger.info(f"\n Repository Authentication Required")
|
|
57
|
+
if is_pypi:
|
|
58
|
+
logger.info(f"PyPI Repository: https://pypi.org/")
|
|
59
|
+
logger.info(" Tip: Use API tokens instead of passwords for better security")
|
|
60
|
+
else:
|
|
61
|
+
logger.info(f"Repository: {args.repository_url}")
|
|
62
|
+
logger.info("-" * 60)
|
|
63
|
+
|
|
64
|
+
if is_pypi:
|
|
65
|
+
logger.info("For PyPI API tokens, use '__token__' as username")
|
|
66
|
+
username_input = input("Username (default: __token__): ").strip()
|
|
67
|
+
username = username_input if username_input else "__token__"
|
|
68
|
+
else:
|
|
69
|
+
username_input = input("Username (default: admin): ").strip()
|
|
70
|
+
username = username_input if username_input else "admin"
|
|
71
|
+
|
|
72
|
+
if username:
|
|
73
|
+
logger.info(f"Using username: {username}")
|
|
74
|
+
|
|
75
|
+
if username == "__token__":
|
|
76
|
+
password = getpass.getpass("API Token (pypi-...): ")
|
|
77
|
+
else:
|
|
78
|
+
password = getpass.getpass("Password: ")
|
|
79
|
+
|
|
80
|
+
if not password:
|
|
81
|
+
logger.info(f"Password/Token cannot be empty")
|
|
82
|
+
raise ValueError("Password/Token cannot be empty")
|
|
83
|
+
|
|
84
|
+
return username, password
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def parse_prerelease(version: str):
|
|
88
|
+
# Match the pattern: numbers.numbers.numbers + optional prerelease type + optional version
|
|
89
|
+
pattern = r'^(\d+)\.(\d+)\.(\d+)([abc]|rc)?(\d*)$'
|
|
90
|
+
match = re.match(pattern, version)
|
|
91
|
+
|
|
92
|
+
if match:
|
|
93
|
+
major = int(match.group(1))
|
|
94
|
+
minor = int(match.group(2))
|
|
95
|
+
patch = int(match.group(3))
|
|
96
|
+
prerelease_type = match.group(4) # None if no prerelease
|
|
97
|
+
|
|
98
|
+
if prerelease_type and match.group(5):
|
|
99
|
+
prerelease_version = int(match.group(5))
|
|
100
|
+
elif prerelease_type:
|
|
101
|
+
prerelease_version = 1
|
|
102
|
+
else:
|
|
103
|
+
prerelease_version = None
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
'major': major,
|
|
107
|
+
'minor': minor,
|
|
108
|
+
'patch': patch,
|
|
109
|
+
'prerelease_type': prerelease_type,
|
|
110
|
+
'prerelease_version': prerelease_version,
|
|
111
|
+
'has_prerelease': prerelease_type is not None
|
|
112
|
+
}
|
|
113
|
+
else:
|
|
114
|
+
raise ValueError(f"Invalid version format: {version}")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_pypirc_info():
|
|
118
|
+
"""
|
|
119
|
+
Read and parse .pypirc file from user's home directory.
|
|
120
|
+
Returns a dictionary with repository configurations.
|
|
121
|
+
"""
|
|
122
|
+
# Get the path to .pypirc file
|
|
123
|
+
home_dir = Path.home()
|
|
124
|
+
pypirc_path = home_dir / '.pypirc'
|
|
125
|
+
|
|
126
|
+
if not pypirc_path.exists():
|
|
127
|
+
raise FileNotFoundError(f"No .pypirc file found at {pypirc_path}")
|
|
128
|
+
|
|
129
|
+
# Parse the configuration file
|
|
130
|
+
config = configparser.ConfigParser()
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
config.read(pypirc_path)
|
|
134
|
+
|
|
135
|
+
# Extract information
|
|
136
|
+
pypirc_info = {}
|
|
137
|
+
|
|
138
|
+
# Get index servers if available
|
|
139
|
+
if config.has_section('distutils') and config.has_option('distutils', 'index-servers'):
|
|
140
|
+
index_servers = config.get('distutils', 'index-servers').split()
|
|
141
|
+
pypirc_info['index_servers'] = index_servers
|
|
142
|
+
|
|
143
|
+
# Get repository configurations
|
|
144
|
+
repositories = {}
|
|
145
|
+
for section_name in config.sections():
|
|
146
|
+
if section_name != 'distutils':
|
|
147
|
+
repo_config = {}
|
|
148
|
+
for option in config.options(section_name):
|
|
149
|
+
repo_config[option] = config.get(section_name, option)
|
|
150
|
+
repositories[section_name] = repo_config
|
|
151
|
+
if not repositories:
|
|
152
|
+
raise ValueError(f"No repositories configuration found in {pypirc_path}")
|
|
153
|
+
|
|
154
|
+
pypirc_info['repositories'] = repositories
|
|
155
|
+
return pypirc_info
|
|
156
|
+
except Exception as e:
|
|
157
|
+
logger.error(f"Error reading .pypirc file: {e}")
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
def ensure_uv_installed():
|
|
161
|
+
"""Check if 'uv' is available, and install it if not."""
|
|
162
|
+
if shutil.which("uv"):
|
|
163
|
+
logger.info("uv is already installed and available in PATH.")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
result = subprocess.run(
|
|
168
|
+
[sys.executable, "-m", "uv", "--version"],
|
|
169
|
+
capture_output=True,
|
|
170
|
+
text=True,
|
|
171
|
+
check=True
|
|
172
|
+
)
|
|
173
|
+
logger.info(f"uv is installed as a module: {result.stdout.strip()}")
|
|
174
|
+
return
|
|
175
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
176
|
+
pass # uv not available as module either
|
|
177
|
+
|
|
178
|
+
logger.info("uv not found. Installing uv via pip...")
|
|
179
|
+
try:
|
|
180
|
+
subprocess.run(
|
|
181
|
+
[sys.executable, "-m", "pip", "install", "uv"],
|
|
182
|
+
check=True,
|
|
183
|
+
capture_output=True,
|
|
184
|
+
text=True
|
|
185
|
+
)
|
|
186
|
+
logger.info("uv installed successfully.")
|
|
187
|
+
except subprocess.CalledProcessError as e:
|
|
188
|
+
logger.error("Failed to install uv:", e.stderr)
|
|
189
|
+
raise RuntimeError(
|
|
190
|
+
"Failed to install uv automatically. Please install it manually:\n"
|
|
191
|
+
" pip install uv\n"
|
|
192
|
+
"Or download from: https://github.com/astral-sh/uv/releases"
|
|
193
|
+
) from e
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
result = subprocess.run(
|
|
197
|
+
[sys.executable, "-m", "uv", "--version"],
|
|
198
|
+
capture_output=True,
|
|
199
|
+
text=True,
|
|
200
|
+
check=True
|
|
201
|
+
)
|
|
202
|
+
logger.info(f"uv version: {result.stdout.strip()}")
|
|
203
|
+
except subprocess.CalledProcessError as e:
|
|
204
|
+
raise RuntimeError("uv installed but failed to run.") from e
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def load_config(pyproject_path: Path) -> TOMLDocument:
|
|
208
|
+
"""load pyproject.toml"""
|
|
209
|
+
if not pyproject_path.exists():
|
|
210
|
+
raise FileNotFoundError(f"pyproject.toml not found at {pyproject_path}")
|
|
211
|
+
|
|
212
|
+
with open(pyproject_path, "r", encoding="utf-8") as f:
|
|
213
|
+
content = f.read()
|
|
214
|
+
config = tomlkit.parse(content)
|
|
215
|
+
return config
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def save_config(config, pyproject_path: Path):
|
|
219
|
+
with open(pyproject_path, 'w', encoding='utf-8') as f:
|
|
220
|
+
f.write(tomlkit.dumps(config))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def validate_version_arg(value: str) -> str:
|
|
224
|
+
"""
|
|
225
|
+
Custom argparse validator that checks if a version string conforms to SemVer.
|
|
226
|
+
Returns the value if valid, raises an error otherwise.
|
|
227
|
+
"""
|
|
228
|
+
version_pattern = re.compile(r"^\d+\.\d+\.\d+(a\d+|b\d+|rc\d+)?$")
|
|
229
|
+
|
|
230
|
+
if not version_pattern.match(value):
|
|
231
|
+
raise argparse.ArgumentTypeError(
|
|
232
|
+
f"Invalid version format: '{value}'. Valid examples:\n"
|
|
233
|
+
" Final release: 1.2.0\n"
|
|
234
|
+
" Alpha release: 1.2.0a1\n"
|
|
235
|
+
" Beta release: 1.2.0b1\n"
|
|
236
|
+
" Release candidate: 1.2.0rc1"
|
|
237
|
+
)
|
|
238
|
+
return value
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from .utils import parse_prerelease, load_config, save_config
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VersionManager:
|
|
12
|
+
"""Version Manager"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, pyproject_path: Path):
|
|
15
|
+
self.pyproject_path = pyproject_path
|
|
16
|
+
self.toml_config = load_config(pyproject_path)
|
|
17
|
+
|
|
18
|
+
def get_current_version(self) -> str:
|
|
19
|
+
return str(self.toml_config['project']['version'])
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def resolve_new_version(current_version: str, version_type: str) -> str:
|
|
23
|
+
version_info = parse_prerelease(current_version)
|
|
24
|
+
major = version_info['major']
|
|
25
|
+
minor = version_info['minor']
|
|
26
|
+
patch = version_info['patch']
|
|
27
|
+
prerelease_type = version_info['prerelease_type']
|
|
28
|
+
prerelease_version = version_info['prerelease_version']
|
|
29
|
+
has_prerelease = version_info['has_prerelease']
|
|
30
|
+
|
|
31
|
+
# Handle version bumping
|
|
32
|
+
if version_type == 'patch':
|
|
33
|
+
prerelease_type = None
|
|
34
|
+
patch += 1
|
|
35
|
+
elif version_type == 'minor':
|
|
36
|
+
minor += 1
|
|
37
|
+
patch = 0
|
|
38
|
+
prerelease_type = None
|
|
39
|
+
elif version_type == 'major':
|
|
40
|
+
major += 1
|
|
41
|
+
minor = 0
|
|
42
|
+
patch = 0
|
|
43
|
+
prerelease_type = None
|
|
44
|
+
elif version_type == 'alpha':
|
|
45
|
+
if has_prerelease:
|
|
46
|
+
if prerelease_type == 'a':
|
|
47
|
+
prerelease_version += 1
|
|
48
|
+
else:
|
|
49
|
+
prerelease_type = 'a'
|
|
50
|
+
prerelease_version = 1
|
|
51
|
+
else:
|
|
52
|
+
prerelease_type = 'a'
|
|
53
|
+
prerelease_version = 1
|
|
54
|
+
elif version_type == 'beta':
|
|
55
|
+
if has_prerelease:
|
|
56
|
+
if prerelease_type == 'a':
|
|
57
|
+
prerelease_type = 'b'
|
|
58
|
+
prerelease_version = 1
|
|
59
|
+
elif prerelease_type == 'b':
|
|
60
|
+
prerelease_version += 1
|
|
61
|
+
else:
|
|
62
|
+
prerelease_type = 'b'
|
|
63
|
+
prerelease_version = 1
|
|
64
|
+
else:
|
|
65
|
+
prerelease_type = 'b'
|
|
66
|
+
prerelease_version = 1
|
|
67
|
+
elif version_type == 'rc':
|
|
68
|
+
if has_prerelease:
|
|
69
|
+
if prerelease_type in ['a', 'b']:
|
|
70
|
+
prerelease_type = 'rc'
|
|
71
|
+
prerelease_version = 1
|
|
72
|
+
elif prerelease_type == 'rc':
|
|
73
|
+
prerelease_version += 1
|
|
74
|
+
else:
|
|
75
|
+
prerelease_type = 'rc'
|
|
76
|
+
prerelease_version = 1
|
|
77
|
+
else:
|
|
78
|
+
raise ValueError(f"Invalid version type: {version_type}")
|
|
79
|
+
|
|
80
|
+
if prerelease_type:
|
|
81
|
+
new_version = f"{major}.{minor}.{patch}{prerelease_type}{prerelease_version}"
|
|
82
|
+
else:
|
|
83
|
+
new_version = f"{major}.{minor}.{patch}"
|
|
84
|
+
return new_version
|
|
85
|
+
|
|
86
|
+
def bump_version(self, version_type: str, new_version: Optional[str]=None) -> str:
|
|
87
|
+
current_version = self.get_current_version()
|
|
88
|
+
if new_version is None:
|
|
89
|
+
new_version = self.resolve_new_version(current_version, version_type)
|
|
90
|
+
# Update pyproject.toml
|
|
91
|
+
self.toml_config['project']['version'] = new_version
|
|
92
|
+
save_config(self.toml_config, self.pyproject_path)
|
|
93
|
+
# Update files configured under [tool.bumpversion.file]
|
|
94
|
+
self.update_bumpversion_files(current_version, new_version)
|
|
95
|
+
|
|
96
|
+
logger.info(f"Version bumped from {current_version} to {new_version}")
|
|
97
|
+
return new_version
|
|
98
|
+
|
|
99
|
+
def update_bumpversion_files(self, current_version: str, new_version: str) -> None:
|
|
100
|
+
"""Update files configured in [tool.bumpversion.file] section."""
|
|
101
|
+
bumpversion_config = self.toml_config.get('tool', {}).get('bumpversion', {})
|
|
102
|
+
files = bumpversion_config.get('file', [])
|
|
103
|
+
|
|
104
|
+
# If 'file' is a single dict, make it a list
|
|
105
|
+
if isinstance(files, dict):
|
|
106
|
+
files = [files]
|
|
107
|
+
|
|
108
|
+
for file_config in files:
|
|
109
|
+
filename = file_config.get('filename')
|
|
110
|
+
if filename == "pyproject.toml":
|
|
111
|
+
continue
|
|
112
|
+
search = file_config.get('search', '{current_version}')
|
|
113
|
+
replace = file_config.get('replace', '{new_version}')
|
|
114
|
+
|
|
115
|
+
if not filename:
|
|
116
|
+
logger.warning("Skipping bumpversion file entry with no 'filename'")
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
file_path = Path(filename)
|
|
120
|
+
if not file_path.exists():
|
|
121
|
+
logger.warning(f"File {filename} not found, skipping.")
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
# Read file content
|
|
125
|
+
content = file_path.read_text(encoding='utf-8')
|
|
126
|
+
|
|
127
|
+
# Replace placeholders
|
|
128
|
+
old_str = search.format(current_version=current_version)
|
|
129
|
+
new_str = replace.format(new_version=new_version)
|
|
130
|
+
|
|
131
|
+
# Escape for literal string replacement (not regex)
|
|
132
|
+
if old_str not in content:
|
|
133
|
+
logger.warning(f"Search pattern '{old_str}' not found in {filename}, skipping.")
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
new_content = content.replace(old_str, new_str)
|
|
137
|
+
|
|
138
|
+
# Write back only if changed
|
|
139
|
+
if new_content != content:
|
|
140
|
+
file_path.write_text(new_content, encoding='utf-8')
|
|
141
|
+
logger.info(f"Updated {filename}: '{old_str}' → '{new_str}'")
|
|
142
|
+
else:
|
|
143
|
+
logger.debug(f"No changes needed in {filename}")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pkg-deploy
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Modern Python Package Deployment Tool
|
|
5
|
+
Author: cw
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: toml
|
|
9
|
+
Requires-Dist: tomlkit
|
|
10
|
+
Requires-Dist: build
|
|
11
|
+
Requires-Dist: twine
|
|
12
|
+
Requires-Dist: Cython
|
|
13
|
+
Requires-Dist: setuptools>=70
|
|
14
|
+
|
|
15
|
+
# Description
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pkg_deploy/__init__.py,sha256=AMFvImM9PajO30Tl53kK6lhL-tZCIq5xTCkWZPrbwUw,58
|
|
2
|
+
pkg_deploy/build.py,sha256=zRg_Hfz_7XRaVvp17N1WEs7dhC77Fao2cvnuh_H5PbA,7450
|
|
3
|
+
pkg_deploy/deploy.py,sha256=lmABig0rerySTPklcK-koBZLXhaYS5UAZ22fGgaXHns,10438
|
|
4
|
+
pkg_deploy/upload.py,sha256=XcB-lArBvNsijoJmrgIHkXodJh4ziUNP5xMJ2wbW7O4,2370
|
|
5
|
+
pkg_deploy/utils.py,sha256=pYfbeii4nvX9Adpkm_XCStKhn1fHQR5LKiMacCRAo2o,8094
|
|
6
|
+
pkg_deploy/version_managment.py,sha256=qLqeNlOTvwP7shnAJYwE5hzTXaCYiyUvtVJ8xsI_ivk,5499
|
|
7
|
+
pkg_deploy-1.0.0.dist-info/METADATA,sha256=VudheNT551T2RyLiu5KfamaWgRsdHx-eL6lp_13nXoo,343
|
|
8
|
+
pkg_deploy-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
pkg_deploy-1.0.0.dist-info/top_level.txt,sha256=-yi8c2TJ5e9J3QATUf7_dRfmJp2gzHR_VvnEc7QgFM0,11
|
|
10
|
+
pkg_deploy-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pkg_deploy
|