pysfi 0.1.11__py3-none-any.whl → 0.1.13__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.
- {pysfi-0.1.11.dist-info → pysfi-0.1.13.dist-info}/METADATA +3 -1
- pysfi-0.1.13.dist-info/RECORD +70 -0
- {pysfi-0.1.11.dist-info → pysfi-0.1.13.dist-info}/entry_points.txt +3 -0
- sfi/__init__.py +5 -3
- sfi/alarmclock/__init__.py +3 -0
- sfi/alarmclock/alarmclock.py +23 -40
- sfi/bumpversion/__init__.py +5 -3
- sfi/cleanbuild/__init__.py +3 -0
- sfi/cli.py +12 -2
- sfi/condasetup/__init__.py +1 -0
- sfi/docdiff/__init__.py +1 -0
- sfi/docdiff/docdiff.py +238 -0
- sfi/docscan/__init__.py +3 -3
- sfi/docscan/docscan_gui.py +150 -46
- sfi/img2pdf/__init__.py +0 -0
- sfi/img2pdf/img2pdf.py +453 -0
- sfi/llmclient/__init__.py +0 -0
- sfi/llmclient/llmclient.py +31 -8
- sfi/llmquantize/llmquantize.py +39 -11
- sfi/llmserver/__init__.py +1 -0
- sfi/llmserver/llmserver.py +63 -13
- sfi/makepython/makepython.py +507 -124
- sfi/pyarchive/__init__.py +1 -0
- sfi/pyarchive/pyarchive.py +908 -278
- sfi/pyembedinstall/pyembedinstall.py +88 -89
- sfi/pylibpack/pylibpack.py +571 -465
- sfi/pyloadergen/pyloadergen.py +372 -218
- sfi/pypack/pypack.py +494 -965
- sfi/pyprojectparse/pyprojectparse.py +328 -28
- sfi/pysourcepack/__init__.py +1 -0
- sfi/pysourcepack/pysourcepack.py +210 -131
- sfi/quizbase/quizbase_gui.py +2 -2
- sfi/taskkill/taskkill.py +168 -59
- sfi/which/which.py +11 -3
- sfi/workflowengine/workflowengine.py +225 -122
- pysfi-0.1.11.dist-info/RECORD +0 -60
- {pysfi-0.1.11.dist-info → pysfi-0.1.13.dist-info}/WHEEL +0 -0
sfi/makepython/makepython.py
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
|
+
"""MakePython: Automated Python project building and management tool.
|
|
2
|
+
|
|
3
|
+
This module provides a command-line interface for common Python development tasks
|
|
4
|
+
including building, testing, publishing, and version management.
|
|
5
|
+
"""
|
|
6
|
+
|
|
1
7
|
from __future__ import annotations
|
|
2
8
|
|
|
9
|
+
__version__ = "0.2.0"
|
|
10
|
+
__author__ = "pysfi Developers"
|
|
11
|
+
__email__ = "developers@pysfi.org"
|
|
3
12
|
import argparse
|
|
13
|
+
import atexit
|
|
14
|
+
import contextlib
|
|
15
|
+
import json
|
|
4
16
|
import logging
|
|
5
17
|
import os
|
|
6
18
|
import shutil
|
|
7
19
|
import subprocess
|
|
8
20
|
import sys
|
|
9
21
|
from dataclasses import dataclass
|
|
22
|
+
from enum import Enum
|
|
23
|
+
from functools import cached_property
|
|
10
24
|
from pathlib import Path
|
|
11
|
-
from typing import Any, Callable
|
|
25
|
+
from typing import Any, Callable, Final, Protocol
|
|
12
26
|
|
|
13
27
|
if sys.version_info >= (3, 11):
|
|
14
28
|
import tomllib
|
|
@@ -16,151 +30,417 @@ else:
|
|
|
16
30
|
import tomli as tomllib # type: ignore
|
|
17
31
|
|
|
18
32
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
33
|
+
# Platform detection
|
|
34
|
+
IS_WINDOWS: Final[bool] = sys.platform == "win32"
|
|
35
|
+
IS_LINUX: Final[bool] = sys.platform == "linux"
|
|
36
|
+
IS_MACOS: Final[bool] = sys.platform == "darwin"
|
|
22
37
|
|
|
23
|
-
|
|
38
|
+
# Logging setup
|
|
39
|
+
logging.basicConfig(
|
|
40
|
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
41
|
+
)
|
|
24
42
|
logger = logging.getLogger(__name__)
|
|
25
|
-
|
|
43
|
+
CURRENT_WORKING_DIR: Final[Path] = Path.cwd()
|
|
26
44
|
|
|
27
|
-
|
|
45
|
+
# Supported build tools
|
|
46
|
+
_BUILD_COMMANDS: Final[list[str]] = ["uv", "poetry", "hatch"]
|
|
47
|
+
CONFIG_FILE: Final[Path] = Path.home() / ".pysfi" / "makepython.json"
|
|
28
48
|
|
|
29
49
|
|
|
30
|
-
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
50
|
+
class BuildTool(Enum):
|
|
51
|
+
"""Enumeration of supported build tools."""
|
|
52
|
+
|
|
53
|
+
UV = "uv"
|
|
54
|
+
POETRY = "poetry"
|
|
55
|
+
HATCH = "hatch"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class CommandType(Enum):
|
|
59
|
+
"""Enumeration of command types."""
|
|
60
|
+
|
|
61
|
+
BUILD = "build"
|
|
62
|
+
CLEAN = "clean"
|
|
63
|
+
TEST = "test"
|
|
64
|
+
PUBLISH = "publish"
|
|
65
|
+
BUMP_VERSION = "bumpversion"
|
|
66
|
+
TOKEN = "token"
|
|
36
67
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class ProjectInfo:
|
|
71
|
+
"""Information about the current Python project."""
|
|
72
|
+
|
|
73
|
+
name: str
|
|
74
|
+
version: str
|
|
75
|
+
build_tool: BuildTool | None = None
|
|
76
|
+
has_tests: bool = False
|
|
77
|
+
has_benchmarks: bool = False
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def from_pyproject(cls, directory: Path) -> ProjectInfo | None:
|
|
81
|
+
"""Create ProjectInfo from pyproject.toml file."""
|
|
82
|
+
pyproject_path = directory / "pyproject.toml"
|
|
83
|
+
if not pyproject_path.exists():
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
with pyproject_path.open("rb") as f:
|
|
88
|
+
data = tomllib.load(f)
|
|
89
|
+
|
|
90
|
+
project_data = data.get("project", {})
|
|
91
|
+
name = project_data.get("name", directory.name)
|
|
92
|
+
version = project_data.get("version", "0.1.0")
|
|
93
|
+
|
|
94
|
+
# Detect build tool
|
|
95
|
+
build_tool = None
|
|
96
|
+
if "build-system" in data:
|
|
97
|
+
backend = data["build-system"].get("build-backend", "")
|
|
98
|
+
if backend.startswith("poetry."):
|
|
99
|
+
build_tool = BuildTool.POETRY
|
|
100
|
+
elif backend.startswith("hatchling."):
|
|
101
|
+
build_tool = BuildTool.HATCH
|
|
102
|
+
else:
|
|
103
|
+
build_tool = BuildTool.UV
|
|
104
|
+
|
|
105
|
+
# Check for tests and benchmarks
|
|
106
|
+
tests_dir = directory / "tests"
|
|
107
|
+
has_tests = tests_dir.exists() and tests_dir.is_dir()
|
|
108
|
+
|
|
109
|
+
benchmarks_dir = directory / "tests" / "benchmarks"
|
|
110
|
+
has_benchmarks = benchmarks_dir.exists() and benchmarks_dir.is_dir()
|
|
111
|
+
|
|
112
|
+
return cls(
|
|
113
|
+
name=name,
|
|
114
|
+
version=version,
|
|
115
|
+
build_tool=build_tool,
|
|
116
|
+
has_tests=has_tests,
|
|
117
|
+
has_benchmarks=has_benchmarks,
|
|
118
|
+
)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.warning(f"Failed to parse pyproject.toml: {e}")
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class MakePythonConfig:
|
|
126
|
+
"""Configuration for MakePython tool."""
|
|
127
|
+
|
|
128
|
+
default_build_tool: BuildTool = BuildTool.UV
|
|
129
|
+
auto_detect_tool: bool = True
|
|
130
|
+
verbose_output: bool = False
|
|
131
|
+
max_retries: int = 3
|
|
132
|
+
timeout_seconds: int = 300
|
|
133
|
+
|
|
134
|
+
def __post_init__(self) -> None:
|
|
135
|
+
"""Initialize configuration after creation."""
|
|
136
|
+
self._load_from_file()
|
|
137
|
+
atexit.register(self.save)
|
|
138
|
+
|
|
139
|
+
def _load_from_file(self) -> None:
|
|
140
|
+
"""Load configuration from file if it exists."""
|
|
141
|
+
if not CONFIG_FILE.exists():
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
config_data = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
|
146
|
+
for key, value in config_data.items():
|
|
147
|
+
if hasattr(self, key):
|
|
148
|
+
if key == "default_build_tool":
|
|
149
|
+
setattr(self, key, BuildTool(value))
|
|
150
|
+
else:
|
|
151
|
+
setattr(self, key, value)
|
|
152
|
+
except (json.JSONDecodeError, TypeError, AttributeError) as e:
|
|
153
|
+
logger.warning(f"Could not load config from {CONFIG_FILE}: {e}")
|
|
154
|
+
|
|
155
|
+
def save(self) -> None:
|
|
156
|
+
"""Save current configuration to file."""
|
|
157
|
+
try:
|
|
158
|
+
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
config_dict = {
|
|
160
|
+
"default_build_tool": self.default_build_tool.value,
|
|
161
|
+
"auto_detect_tool": self.auto_detect_tool,
|
|
162
|
+
"verbose_output": self.verbose_output,
|
|
163
|
+
"max_retries": self.max_retries,
|
|
164
|
+
"timeout_seconds": self.timeout_seconds,
|
|
165
|
+
}
|
|
166
|
+
CONFIG_FILE.write_text(json.dumps(config_dict, indent=4), encoding="utf-8")
|
|
167
|
+
logger.debug(f"Configuration saved to {CONFIG_FILE}")
|
|
168
|
+
except OSError as e:
|
|
169
|
+
logger.error(f"Failed to save configuration: {e}")
|
|
170
|
+
|
|
171
|
+
@cached_property
|
|
172
|
+
def supported_tools(self) -> list[BuildTool]:
|
|
173
|
+
"""Get list of available build tools."""
|
|
174
|
+
available_tools = []
|
|
175
|
+
for tool in BuildTool:
|
|
176
|
+
if shutil.which(tool.value):
|
|
177
|
+
available_tools.append(tool)
|
|
178
|
+
return available_tools
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class CommandExecutor(Protocol):
|
|
182
|
+
"""Protocol for command execution."""
|
|
183
|
+
|
|
184
|
+
def execute(
|
|
185
|
+
self, command: list[str], cwd: Path
|
|
186
|
+
) -> subprocess.CompletedProcess[str]: ...
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class SubprocessExecutor:
|
|
190
|
+
"""Execute commands using subprocess."""
|
|
191
|
+
|
|
192
|
+
def __init__(self, config: MakePythonConfig):
|
|
193
|
+
self.config = config
|
|
194
|
+
|
|
195
|
+
def execute(
|
|
196
|
+
self, command: list[str], cwd: Path
|
|
197
|
+
) -> subprocess.CompletedProcess[str]:
|
|
198
|
+
"""Execute command with proper error handling."""
|
|
199
|
+
logger.debug(f"Executing command: {' '.join(command)} in {cwd}")
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
result = subprocess.run(
|
|
203
|
+
command,
|
|
204
|
+
cwd=cwd,
|
|
205
|
+
capture_output=True,
|
|
206
|
+
text=True,
|
|
207
|
+
encoding="utf-8",
|
|
208
|
+
errors="replace",
|
|
209
|
+
timeout=self.config.timeout_seconds,
|
|
210
|
+
check=True,
|
|
211
|
+
)
|
|
212
|
+
return result
|
|
213
|
+
except subprocess.TimeoutExpired:
|
|
214
|
+
logger.error(
|
|
215
|
+
f"Command timed out after {self.config.timeout_seconds} seconds"
|
|
216
|
+
)
|
|
217
|
+
raise
|
|
218
|
+
except subprocess.CalledProcessError as e:
|
|
219
|
+
logger.error(f"Command failed with exit code {e.returncode}")
|
|
220
|
+
if e.stdout:
|
|
221
|
+
logger.error(f"STDOUT: {e.stdout}")
|
|
222
|
+
if e.stderr:
|
|
223
|
+
logger.error(f"STDERR: {e.stderr}")
|
|
224
|
+
raise
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.error(f"Unexpected error executing command: {e}")
|
|
227
|
+
raise
|
|
43
228
|
|
|
44
229
|
|
|
45
230
|
def _get_build_command_from_toml(directory: Path) -> str | None:
|
|
46
231
|
"""Get build command from pyproject.toml."""
|
|
47
232
|
logger.debug(f"Parsing pyproject.toml in {directory}")
|
|
48
233
|
|
|
49
|
-
|
|
50
|
-
if not
|
|
234
|
+
project_info = ProjectInfo.from_pyproject(directory)
|
|
235
|
+
if not project_info or not project_info.build_tool:
|
|
51
236
|
return None
|
|
52
237
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if "build-backend" in build_system:
|
|
56
|
-
build_backend = build_system["build-backend"]
|
|
57
|
-
if build_backend.startswith("poetry."):
|
|
58
|
-
return "poetry"
|
|
59
|
-
elif build_backend.startswith("hatchling."):
|
|
60
|
-
return "hatch"
|
|
61
|
-
else:
|
|
62
|
-
logger.error(f"Unknown build-backend: {build_backend}")
|
|
63
|
-
return None
|
|
64
|
-
|
|
65
|
-
logger.error("No `build-system` or `build-backend` found in pyproject.toml")
|
|
66
|
-
return None
|
|
238
|
+
return project_info.build_tool.value
|
|
239
|
+
|
|
67
240
|
|
|
241
|
+
def _get_build_command(directory: Path, config: MakePythonConfig) -> str | None:
|
|
242
|
+
"""Get build command from directory using configuration.
|
|
68
243
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
244
|
+
Args:
|
|
245
|
+
directory: Directory to search for project configuration
|
|
246
|
+
config: MakePython configuration object
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Build command string or None if not found
|
|
250
|
+
"""
|
|
251
|
+
project_info = ProjectInfo.from_pyproject(directory)
|
|
252
|
+
if project_info and project_info.build_tool:
|
|
253
|
+
logger.debug(f"Detected build tool: {project_info.build_tool.value}")
|
|
254
|
+
return project_info.build_tool.value
|
|
255
|
+
|
|
256
|
+
# Fallback to available tools
|
|
257
|
+
if config.supported_tools:
|
|
258
|
+
selected_tool = config.supported_tools[0]
|
|
259
|
+
logger.debug(f"Using available build tool: {selected_tool.value}")
|
|
260
|
+
return selected_tool.value
|
|
75
261
|
|
|
76
|
-
for command in _BUILD_COMMANDS:
|
|
77
|
-
if shutil.which(command):
|
|
78
|
-
logger.debug(f"Found build command: {command}")
|
|
79
|
-
return command
|
|
80
262
|
logger.error(f"No build command found in {directory}")
|
|
263
|
+
logger.error("Please install uv, poetry, or hatch")
|
|
81
264
|
sys.exit(1)
|
|
82
265
|
return None
|
|
83
266
|
|
|
84
267
|
|
|
85
268
|
@dataclass
|
|
86
269
|
class Command:
|
|
270
|
+
"""Represents a command that can be executed."""
|
|
271
|
+
|
|
87
272
|
name: str
|
|
88
273
|
alias: str
|
|
274
|
+
command_type: CommandType
|
|
89
275
|
cmds: list[str] | Callable[..., Any] | None = None
|
|
276
|
+
description: str = ""
|
|
90
277
|
|
|
91
278
|
|
|
92
|
-
def _clean(root_dir: Path =
|
|
93
|
-
|
|
279
|
+
def _clean(root_dir: Path = CURRENT_WORKING_DIR) -> None:
|
|
280
|
+
"""Clean build artifacts and temporary files."""
|
|
281
|
+
clean_commands = [
|
|
282
|
+
["rm", "-rf", "dist"],
|
|
283
|
+
["rm", "-rf", "build"],
|
|
284
|
+
["rm", "-rf", "*.egg-info"],
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
for cmd in clean_commands:
|
|
288
|
+
with contextlib.suppress(subprocess.CalledProcessError):
|
|
289
|
+
subprocess.run(cmd, cwd=root_dir, capture_output=True, check=True)
|
|
94
290
|
|
|
95
291
|
|
|
96
292
|
def main() -> None:
|
|
293
|
+
"""Main entry point for MakePython tool."""
|
|
294
|
+
# Initialize configuration
|
|
295
|
+
config = MakePythonConfig()
|
|
296
|
+
|
|
97
297
|
# Get build command
|
|
98
|
-
build_command = _get_build_command(
|
|
298
|
+
build_command = _get_build_command(CURRENT_WORKING_DIR, config) or ""
|
|
299
|
+
# Create commands with proper structure
|
|
99
300
|
commands = [
|
|
100
|
-
Command(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
Command(
|
|
301
|
+
Command(
|
|
302
|
+
name="build",
|
|
303
|
+
alias="b",
|
|
304
|
+
command_type=CommandType.BUILD,
|
|
305
|
+
cmds=[build_command, "build"],
|
|
306
|
+
description="Build the project",
|
|
307
|
+
),
|
|
308
|
+
Command(
|
|
309
|
+
name="bumpversion",
|
|
310
|
+
alias="bump",
|
|
311
|
+
command_type=CommandType.BUMP_VERSION,
|
|
312
|
+
cmds=["bumpversion", "patch", "--tag"],
|
|
313
|
+
description="Bump project version",
|
|
314
|
+
),
|
|
315
|
+
Command(
|
|
316
|
+
name="clean",
|
|
317
|
+
alias="c",
|
|
318
|
+
command_type=CommandType.CLEAN,
|
|
319
|
+
cmds=_clean,
|
|
320
|
+
description="Clean build artifacts",
|
|
321
|
+
),
|
|
322
|
+
Command(
|
|
323
|
+
name="publish",
|
|
324
|
+
alias="p",
|
|
325
|
+
command_type=CommandType.PUBLISH,
|
|
326
|
+
description="Publish package to PyPI",
|
|
327
|
+
),
|
|
328
|
+
Command(
|
|
329
|
+
name="test",
|
|
330
|
+
alias="t",
|
|
331
|
+
command_type=CommandType.TEST,
|
|
332
|
+
cmds=lambda: os.system("pytest"),
|
|
333
|
+
description="Run tests",
|
|
334
|
+
),
|
|
335
|
+
Command(
|
|
336
|
+
name="test-benchmark",
|
|
337
|
+
alias="tb",
|
|
338
|
+
command_type=CommandType.TEST,
|
|
339
|
+
cmds=lambda: os.system("pytest -m benchmark"),
|
|
340
|
+
description="Run benchmark tests",
|
|
341
|
+
),
|
|
342
|
+
Command(
|
|
343
|
+
name="test-coverage",
|
|
344
|
+
alias="tc",
|
|
345
|
+
command_type=CommandType.TEST,
|
|
346
|
+
cmds=lambda: os.system("pytest --cov=sfi"),
|
|
347
|
+
description="Run tests with coverage",
|
|
348
|
+
),
|
|
349
|
+
Command(
|
|
350
|
+
name="token",
|
|
351
|
+
alias="tk",
|
|
352
|
+
command_type=CommandType.TOKEN,
|
|
353
|
+
cmds=lambda: _set_token(build_command),
|
|
354
|
+
description="Set PyPI token",
|
|
355
|
+
),
|
|
108
356
|
]
|
|
109
357
|
command_dict = {command.name: command for command in commands}
|
|
110
358
|
command_dict.update({command.alias: command for command in commands})
|
|
111
359
|
choices = [command.alias for command in commands]
|
|
112
360
|
choices.extend([command.name for command in commands])
|
|
113
361
|
|
|
114
|
-
# Parse
|
|
115
|
-
parser = argparse.ArgumentParser(
|
|
116
|
-
|
|
117
|
-
|
|
362
|
+
# Parse arguments
|
|
363
|
+
parser = argparse.ArgumentParser(
|
|
364
|
+
prog="makepython",
|
|
365
|
+
description="Automated Python project building and management tool",
|
|
366
|
+
)
|
|
367
|
+
parser.add_argument(
|
|
368
|
+
"--debug",
|
|
369
|
+
"-d",
|
|
370
|
+
action="store_true",
|
|
371
|
+
help="Enable debug mode with verbose output",
|
|
372
|
+
)
|
|
373
|
+
parser.add_argument(
|
|
374
|
+
"--verbose", "-v", action="store_true", help="Enable verbose output"
|
|
375
|
+
)
|
|
376
|
+
parser.add_argument(
|
|
377
|
+
"command",
|
|
378
|
+
type=str,
|
|
379
|
+
choices=choices,
|
|
380
|
+
help=f"Command to run. Options: {', '.join(choices)}",
|
|
381
|
+
)
|
|
118
382
|
args = parser.parse_args()
|
|
383
|
+
|
|
384
|
+
# Set logging level
|
|
119
385
|
if args.debug:
|
|
120
386
|
logger.setLevel(logging.DEBUG)
|
|
387
|
+
config.verbose_output = True
|
|
388
|
+
elif args.verbose:
|
|
389
|
+
logger.setLevel(logging.INFO)
|
|
390
|
+
config.verbose_output = True
|
|
121
391
|
|
|
122
392
|
logger.debug(f"Using build command: {build_command}")
|
|
393
|
+
|
|
394
|
+
# Execute command
|
|
123
395
|
command = command_dict.get(args.command)
|
|
124
396
|
if command:
|
|
125
397
|
if callable(command.cmds):
|
|
126
398
|
command.cmds()
|
|
127
399
|
elif isinstance(command.cmds, list):
|
|
128
|
-
_run_command(command.cmds,
|
|
400
|
+
_run_command(command.cmds, CURRENT_WORKING_DIR)
|
|
129
401
|
else:
|
|
130
|
-
logger.debug("No preset commands found")
|
|
402
|
+
logger.debug("No preset commands found for this command type")
|
|
131
403
|
else:
|
|
132
404
|
logger.error(f"Unknown command: {args.command}")
|
|
133
405
|
sys.exit(1)
|
|
134
406
|
|
|
407
|
+
# Special handling for publish command
|
|
135
408
|
if args.command in {"publish", "p"}:
|
|
136
409
|
if not _check_pypi_token(build_command):
|
|
137
410
|
_set_token(build_command)
|
|
138
|
-
_run_command([build_command, "publish"],
|
|
411
|
+
_run_command([build_command, "publish"], CURRENT_WORKING_DIR)
|
|
139
412
|
|
|
140
413
|
|
|
141
414
|
def _set_token(build_command: str, show_header: bool = True) -> None:
|
|
142
|
-
"""Set PyPI token for the specified build command.
|
|
415
|
+
"""Set PyPI token for the specified build command.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
build_command: The build tool to configure (uv, poetry, hatch)
|
|
419
|
+
show_header: Whether to show the header message
|
|
420
|
+
"""
|
|
143
421
|
if show_header:
|
|
144
422
|
logger.info(f"Setting PyPI token for {build_command}...")
|
|
145
423
|
|
|
146
|
-
if build_command.lower() not in
|
|
424
|
+
if build_command.lower() not in [tool.value for tool in BuildTool]:
|
|
147
425
|
logger.error(f"Unknown build command: {build_command}")
|
|
148
|
-
logger.error(f"Please use
|
|
426
|
+
logger.error(f"Please use one of: {[tool.value for tool in BuildTool]}")
|
|
149
427
|
sys.exit(1)
|
|
150
428
|
|
|
151
429
|
token = input("Enter your PyPI token (leave empty to cancel): ").strip()
|
|
152
430
|
if not token:
|
|
153
|
-
logger.info("
|
|
431
|
+
logger.info("Operation cancelled - no token provided.")
|
|
154
432
|
return
|
|
155
433
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
logger.
|
|
434
|
+
try:
|
|
435
|
+
if build_command == BuildTool.UV.value:
|
|
436
|
+
_set_uv_token(token)
|
|
437
|
+
elif build_command == BuildTool.POETRY.value:
|
|
438
|
+
_set_poetry_token(token)
|
|
439
|
+
elif build_command == BuildTool.HATCH.value:
|
|
440
|
+
_set_hatch_token(token)
|
|
441
|
+
logger.info("PyPI token configured successfully!")
|
|
442
|
+
except Exception as e:
|
|
443
|
+
logger.error(f"Failed to set token: {e}")
|
|
164
444
|
sys.exit(1)
|
|
165
445
|
|
|
166
446
|
|
|
@@ -181,7 +461,7 @@ token = "{token}"
|
|
|
181
461
|
def _set_poetry_token(token: str) -> None:
|
|
182
462
|
"""Set PyPI token for poetry."""
|
|
183
463
|
_write_to_env_file("POETRY_PYPI_TOKEN_PYPI", token)
|
|
184
|
-
_run_command(["poetry", "config", "pypi-token.pypi", token],
|
|
464
|
+
_run_command(["poetry", "config", "pypi-token.pypi", token], CURRENT_WORKING_DIR)
|
|
185
465
|
logger.info("Token saved to Poetry configuration.")
|
|
186
466
|
|
|
187
467
|
|
|
@@ -197,71 +477,168 @@ password = {token}
|
|
|
197
477
|
logger.info(f"Token saved to {pypirc_path}")
|
|
198
478
|
|
|
199
479
|
|
|
200
|
-
def
|
|
201
|
-
"""Check if PyPI token is configured
|
|
202
|
-
|
|
480
|
+
def _check_uv_token() -> bool:
|
|
481
|
+
"""Check if PyPI token is configured for uv."""
|
|
482
|
+
# Check environment variables
|
|
483
|
+
token_env_vars = ["UV_PUBLISH_TOKEN", "PYPI_API_TOKEN"]
|
|
484
|
+
for var in token_env_vars:
|
|
485
|
+
if os.getenv(var):
|
|
486
|
+
logger.info(f"Found PyPI token in environment variable: {var}")
|
|
487
|
+
return True
|
|
488
|
+
|
|
489
|
+
# Check config file
|
|
490
|
+
config_path = Path.home() / ".config" / "uv" / "uv.toml"
|
|
491
|
+
if config_path.exists():
|
|
492
|
+
logger.info(f"Found uv config file: {config_path}")
|
|
493
|
+
return True
|
|
494
|
+
|
|
495
|
+
return False
|
|
496
|
+
|
|
203
497
|
|
|
204
|
-
|
|
205
|
-
if
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
# Check for config file
|
|
215
|
-
config_path = Path.home() / ".config" / "uv" / "uv.toml"
|
|
216
|
-
if config_path.exists():
|
|
217
|
-
logger.info(f"Found uv config file: {config_path}")
|
|
218
|
-
token_found = True
|
|
219
|
-
|
|
220
|
-
elif build_command == "poetry":
|
|
221
|
-
# Check for poetry token
|
|
222
|
-
if os.getenv("POETRY_PYPI_TOKEN_PYPI"):
|
|
223
|
-
logger.info("Found PyPI token in POETRY_PYPI_TOKEN_PYPI environment variable")
|
|
224
|
-
token_found = True
|
|
225
|
-
|
|
226
|
-
# Check for poetry config
|
|
498
|
+
def _check_poetry_token() -> bool:
|
|
499
|
+
"""Check if PyPI token is configured for poetry."""
|
|
500
|
+
# Check environment variable
|
|
501
|
+
if os.getenv("POETRY_PYPI_TOKEN_PYPI"):
|
|
502
|
+
logger.info("Found PyPI token in POETRY_PYPI_TOKEN_PYPI environment variable")
|
|
503
|
+
return True
|
|
504
|
+
|
|
505
|
+
# Check poetry config
|
|
506
|
+
try:
|
|
227
507
|
result = subprocess.run(
|
|
228
508
|
["poetry", "config", "pypi-token.pypi"],
|
|
229
509
|
capture_output=True,
|
|
230
510
|
text=True,
|
|
511
|
+
encoding="utf-8",
|
|
512
|
+
errors="replace",
|
|
513
|
+
check=True,
|
|
231
514
|
)
|
|
232
515
|
if result.stdout.strip() and result.stdout.strip() != "None":
|
|
233
516
|
logger.info("Found PyPI token in Poetry configuration")
|
|
234
|
-
|
|
517
|
+
return True
|
|
518
|
+
except (subprocess.CalledProcessError, Exception) as e:
|
|
519
|
+
logger.error(f"Error checking Poetry token: {e}")
|
|
235
520
|
|
|
236
|
-
|
|
237
|
-
# Check for .pypirc
|
|
238
|
-
pypirc_path = Path.home() / ".pypirc"
|
|
239
|
-
if pypirc_path.exists():
|
|
240
|
-
logger.info(f"Found .pypirc file: {pypirc_path}")
|
|
241
|
-
token_found = True
|
|
521
|
+
return False
|
|
242
522
|
|
|
243
|
-
return token_found
|
|
244
523
|
|
|
524
|
+
def _check_hatch_token() -> bool:
|
|
525
|
+
"""Check if PyPI token is configured for hatch."""
|
|
526
|
+
pypirc_path = Path.home() / ".pypirc"
|
|
527
|
+
if pypirc_path.exists():
|
|
528
|
+
logger.info(f"Found .pypirc file: {pypirc_path}")
|
|
529
|
+
return True
|
|
530
|
+
return False
|
|
245
531
|
|
|
246
|
-
def _run_command(cmd: list[str], directory: Path) -> None:
|
|
247
|
-
"""Run a command in the specified directory."""
|
|
248
|
-
logger.debug(f"Running command: {' '.join(cmd)}")
|
|
249
|
-
try:
|
|
250
|
-
result = subprocess.run(cmd, cwd=directory, capture_output=True, text=True)
|
|
251
|
-
except subprocess.CalledProcessError as e:
|
|
252
|
-
logger.error(f"Command failed with exit code {e.returncode}")
|
|
253
|
-
sys.exit(e.returncode)
|
|
254
532
|
|
|
533
|
+
def _check_pypi_token(build_command: str) -> bool:
|
|
534
|
+
"""Check if PyPI token is configured before publishing."""
|
|
535
|
+
logger.info("Checking PyPI token configuration...")
|
|
536
|
+
|
|
537
|
+
# Map build commands to their respective check functions
|
|
538
|
+
checker_map = {
|
|
539
|
+
"uv": _check_uv_token,
|
|
540
|
+
"poetry": _check_poetry_token,
|
|
541
|
+
"hatch": _check_hatch_token,
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
checker = checker_map.get(build_command.lower())
|
|
545
|
+
if not checker:
|
|
546
|
+
logger.error(f"Unknown build command for token checking: {build_command}")
|
|
547
|
+
return False
|
|
548
|
+
|
|
549
|
+
return checker()
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _execute_subprocess(
|
|
553
|
+
cmd: list[str], directory: Path, encoding: str = "utf-8"
|
|
554
|
+
) -> subprocess.CompletedProcess[str]:
|
|
555
|
+
"""Execute subprocess with specified encoding."""
|
|
556
|
+
return subprocess.run(
|
|
557
|
+
cmd,
|
|
558
|
+
cwd=directory,
|
|
559
|
+
capture_output=True,
|
|
560
|
+
text=True,
|
|
561
|
+
encoding=encoding,
|
|
562
|
+
errors="replace",
|
|
563
|
+
check=True,
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _handle_command_result(result: subprocess.CompletedProcess[str]) -> None:
|
|
568
|
+
"""Handle and output command results."""
|
|
255
569
|
if result.stdout:
|
|
256
570
|
print(result.stdout)
|
|
257
571
|
if result.stderr:
|
|
258
572
|
print(result.stderr, file=sys.stderr)
|
|
259
573
|
|
|
260
574
|
|
|
575
|
+
def _handle_called_process_error(e: subprocess.CalledProcessError) -> None:
|
|
576
|
+
"""Handle CalledProcessError and exit."""
|
|
577
|
+
logger.error(f"Command failed with exit code {e.returncode}")
|
|
578
|
+
if e.stdout:
|
|
579
|
+
logger.error(f"STDOUT: {e.stdout}")
|
|
580
|
+
if e.stderr:
|
|
581
|
+
logger.error(f"STDERR: {e.stderr}")
|
|
582
|
+
sys.exit(e.returncode)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _handle_unicode_decode_error(
|
|
586
|
+
cmd: list[str], directory: Path, error: UnicodeDecodeError
|
|
587
|
+
) -> None:
|
|
588
|
+
"""Handle UnicodeDecodeError with fallback encoding."""
|
|
589
|
+
logger.error(f"Encoding error while processing command output: {error}")
|
|
590
|
+
try:
|
|
591
|
+
result = _execute_subprocess(cmd, directory, "gbk" if IS_WINDOWS else "utf-8")
|
|
592
|
+
_handle_command_result(result)
|
|
593
|
+
except Exception as fallback_error:
|
|
594
|
+
logger.error(f"Fallback encoding also failed: {fallback_error}")
|
|
595
|
+
sys.exit(1)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _run_command(cmd: list[str], directory: Path) -> None:
|
|
599
|
+
"""Run a command in the specified directory with proper error handling.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
cmd: Command to execute as list of arguments
|
|
603
|
+
directory: Directory to run the command in
|
|
604
|
+
|
|
605
|
+
Raises:
|
|
606
|
+
SystemExit: If command fails to execute
|
|
607
|
+
"""
|
|
608
|
+
logger.debug(f"Running command: {' '.join(cmd)} in {directory}")
|
|
609
|
+
|
|
610
|
+
try:
|
|
611
|
+
result = _execute_subprocess(cmd, directory)
|
|
612
|
+
_handle_command_result(result)
|
|
613
|
+
except subprocess.CalledProcessError as e:
|
|
614
|
+
_handle_called_process_error(e)
|
|
615
|
+
except UnicodeDecodeError as e:
|
|
616
|
+
_handle_unicode_decode_error(cmd, directory, e)
|
|
617
|
+
except Exception as e:
|
|
618
|
+
logger.error(f"Unexpected error executing command: {e}")
|
|
619
|
+
sys.exit(1)
|
|
620
|
+
|
|
621
|
+
|
|
261
622
|
def _write_to_env_file(key: str, value: str) -> None:
|
|
262
|
-
"""Write key-value pair to environment file.
|
|
263
|
-
|
|
264
|
-
|
|
623
|
+
"""Write key-value pair to environment file.
|
|
624
|
+
|
|
625
|
+
Args:
|
|
626
|
+
key: Environment variable name
|
|
627
|
+
value: Environment variable value
|
|
628
|
+
"""
|
|
629
|
+
if IS_WINDOWS:
|
|
630
|
+
try:
|
|
631
|
+
subprocess.run(
|
|
632
|
+
["setx", key, value],
|
|
633
|
+
shell=True,
|
|
634
|
+
encoding="utf-8",
|
|
635
|
+
errors="replace",
|
|
636
|
+
check=True,
|
|
637
|
+
)
|
|
638
|
+
logger.info(f"Environment variable {key} set successfully")
|
|
639
|
+
except Exception as e:
|
|
640
|
+
logger.warning(f"Failed to set environment variable {key}: {e}")
|
|
641
|
+
logger.info("You may need to restart your shell for changes to take effect")
|
|
265
642
|
else:
|
|
266
643
|
_write_to_shell_config(f"export {key}='{value}'")
|
|
267
644
|
|
|
@@ -296,7 +673,9 @@ def _write_to_shell_config(content: str) -> None:
|
|
|
296
673
|
break
|
|
297
674
|
|
|
298
675
|
if not var_name:
|
|
299
|
-
logger.error(
|
|
676
|
+
logger.error(
|
|
677
|
+
"Invalid export statement format. Expected: export VARIABLE_NAME=value"
|
|
678
|
+
)
|
|
300
679
|
return
|
|
301
680
|
|
|
302
681
|
# Read existing content
|
|
@@ -307,7 +686,9 @@ def _write_to_shell_config(content: str) -> None:
|
|
|
307
686
|
found_existing = False
|
|
308
687
|
for line in existing_lines:
|
|
309
688
|
# Check if this line exports the same variable
|
|
310
|
-
if line.strip().startswith(f"export {var_name}=") or line.strip().startswith(
|
|
689
|
+
if line.strip().startswith(f"export {var_name}=") or line.strip().startswith(
|
|
690
|
+
f"export {var_name} ="
|
|
691
|
+
):
|
|
311
692
|
found_existing = True
|
|
312
693
|
continue
|
|
313
694
|
new_lines.append(line)
|
|
@@ -324,4 +705,6 @@ def _write_to_shell_config(content: str) -> None:
|
|
|
324
705
|
logger.info(f"Content written to {config_path}")
|
|
325
706
|
logger.info(f"Run `source {config_path}` to apply the changes")
|
|
326
707
|
logger.info(f"Run `cat {config_path}` to view the content")
|
|
327
|
-
logger.info(
|
|
708
|
+
logger.info(
|
|
709
|
+
f"Run `cat {config_path} | grep 'export'` to view the exported variables"
|
|
710
|
+
)
|