crackerjack 0.30.3__py3-none-any.whl → 0.31.7__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.
Potentially problematic release.
This version of crackerjack might be problematic. Click here for more details.
- crackerjack/CLAUDE.md +1005 -0
- crackerjack/RULES.md +380 -0
- crackerjack/__init__.py +42 -13
- crackerjack/__main__.py +227 -299
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +170 -0
- crackerjack/agents/coordinator.py +512 -0
- crackerjack/agents/documentation_agent.py +498 -0
- crackerjack/agents/dry_agent.py +388 -0
- crackerjack/agents/formatting_agent.py +245 -0
- crackerjack/agents/import_optimization_agent.py +281 -0
- crackerjack/agents/performance_agent.py +669 -0
- crackerjack/agents/proactive_agent.py +104 -0
- crackerjack/agents/refactoring_agent.py +788 -0
- crackerjack/agents/security_agent.py +529 -0
- crackerjack/agents/test_creation_agent.py +657 -0
- crackerjack/agents/test_specialist_agent.py +486 -0
- crackerjack/agents/tracker.py +212 -0
- crackerjack/api.py +560 -0
- crackerjack/cli/__init__.py +24 -0
- crackerjack/cli/facade.py +104 -0
- crackerjack/cli/handlers.py +267 -0
- crackerjack/cli/interactive.py +471 -0
- crackerjack/cli/options.py +409 -0
- crackerjack/cli/utils.py +18 -0
- crackerjack/code_cleaner.py +618 -928
- crackerjack/config/__init__.py +19 -0
- crackerjack/config/hooks.py +218 -0
- crackerjack/core/__init__.py +0 -0
- crackerjack/core/async_workflow_orchestrator.py +406 -0
- crackerjack/core/autofix_coordinator.py +200 -0
- crackerjack/core/container.py +104 -0
- crackerjack/core/enhanced_container.py +542 -0
- crackerjack/core/performance.py +243 -0
- crackerjack/core/phase_coordinator.py +585 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +826 -0
- crackerjack/dynamic_config.py +94 -103
- crackerjack/errors.py +263 -41
- crackerjack/executors/__init__.py +11 -0
- crackerjack/executors/async_hook_executor.py +431 -0
- crackerjack/executors/cached_hook_executor.py +242 -0
- crackerjack/executors/hook_executor.py +345 -0
- crackerjack/executors/individual_hook_executor.py +669 -0
- crackerjack/intelligence/__init__.py +44 -0
- crackerjack/intelligence/adaptive_learning.py +751 -0
- crackerjack/intelligence/agent_orchestrator.py +551 -0
- crackerjack/intelligence/agent_registry.py +414 -0
- crackerjack/intelligence/agent_selector.py +502 -0
- crackerjack/intelligence/integration.py +290 -0
- crackerjack/interactive.py +576 -315
- crackerjack/managers/__init__.py +11 -0
- crackerjack/managers/async_hook_manager.py +135 -0
- crackerjack/managers/hook_manager.py +137 -0
- crackerjack/managers/publish_manager.py +433 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +443 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +114 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +621 -0
- crackerjack/mcp/dashboard.py +636 -0
- crackerjack/mcp/enhanced_progress_monitor.py +479 -0
- crackerjack/mcp/file_monitor.py +336 -0
- crackerjack/mcp/progress_components.py +569 -0
- crackerjack/mcp/progress_monitor.py +949 -0
- crackerjack/mcp/rate_limiter.py +332 -0
- crackerjack/mcp/server.py +22 -0
- crackerjack/mcp/server_core.py +244 -0
- crackerjack/mcp/service_watchdog.py +501 -0
- crackerjack/mcp/state.py +395 -0
- crackerjack/mcp/task_manager.py +257 -0
- crackerjack/mcp/tools/__init__.py +17 -0
- crackerjack/mcp/tools/core_tools.py +249 -0
- crackerjack/mcp/tools/error_analyzer.py +308 -0
- crackerjack/mcp/tools/execution_tools.py +372 -0
- crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
- crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
- crackerjack/mcp/tools/intelligence_tools.py +314 -0
- crackerjack/mcp/tools/monitoring_tools.py +502 -0
- crackerjack/mcp/tools/proactive_tools.py +384 -0
- crackerjack/mcp/tools/progress_tools.py +217 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +565 -0
- crackerjack/mcp/websocket/__init__.py +14 -0
- crackerjack/mcp/websocket/app.py +39 -0
- crackerjack/mcp/websocket/endpoints.py +559 -0
- crackerjack/mcp/websocket/jobs.py +253 -0
- crackerjack/mcp/websocket/server.py +116 -0
- crackerjack/mcp/websocket/websocket_handler.py +78 -0
- crackerjack/mcp/websocket_server.py +10 -0
- crackerjack/models/__init__.py +31 -0
- crackerjack/models/config.py +93 -0
- crackerjack/models/config_adapter.py +230 -0
- crackerjack/models/protocols.py +118 -0
- crackerjack/models/task.py +154 -0
- crackerjack/monitoring/ai_agent_watchdog.py +450 -0
- crackerjack/monitoring/regression_prevention.py +638 -0
- crackerjack/orchestration/__init__.py +0 -0
- crackerjack/orchestration/advanced_orchestrator.py +970 -0
- crackerjack/orchestration/coverage_improvement.py +223 -0
- crackerjack/orchestration/execution_strategies.py +341 -0
- crackerjack/orchestration/test_progress_streamer.py +636 -0
- crackerjack/plugins/__init__.py +15 -0
- crackerjack/plugins/base.py +200 -0
- crackerjack/plugins/hooks.py +246 -0
- crackerjack/plugins/loader.py +335 -0
- crackerjack/plugins/managers.py +259 -0
- crackerjack/py313.py +8 -3
- crackerjack/services/__init__.py +22 -0
- crackerjack/services/cache.py +314 -0
- crackerjack/services/config.py +358 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +356 -0
- crackerjack/services/debug.py +736 -0
- crackerjack/services/dependency_monitor.py +617 -0
- crackerjack/services/enhanced_filesystem.py +439 -0
- crackerjack/services/file_hasher.py +151 -0
- crackerjack/services/filesystem.py +421 -0
- crackerjack/services/git.py +176 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +873 -0
- crackerjack/services/log_manager.py +286 -0
- crackerjack/services/logging.py +174 -0
- crackerjack/services/metrics.py +578 -0
- crackerjack/services/pattern_cache.py +362 -0
- crackerjack/services/pattern_detector.py +515 -0
- crackerjack/services/performance_benchmarks.py +653 -0
- crackerjack/services/security.py +163 -0
- crackerjack/services/server_manager.py +234 -0
- crackerjack/services/smart_scheduling.py +144 -0
- crackerjack/services/tool_version_service.py +61 -0
- crackerjack/services/unified_config.py +437 -0
- crackerjack/services/version_checker.py +248 -0
- crackerjack/slash_commands/__init__.py +14 -0
- crackerjack/slash_commands/init.md +122 -0
- crackerjack/slash_commands/run.md +163 -0
- crackerjack/slash_commands/status.md +127 -0
- crackerjack-0.31.7.dist-info/METADATA +742 -0
- crackerjack-0.31.7.dist-info/RECORD +149 -0
- crackerjack-0.31.7.dist-info/entry_points.txt +2 -0
- crackerjack/.gitignore +0 -34
- crackerjack/.libcst.codemod.yaml +0 -18
- crackerjack/.pdm.toml +0 -1
- crackerjack/crackerjack.py +0 -3805
- crackerjack/pyproject.toml +0 -286
- crackerjack-0.30.3.dist-info/METADATA +0 -1290
- crackerjack-0.30.3.dist-info/RECORD +0 -16
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/WHEEL +0 -0
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import typing as t
|
|
3
|
+
from contextlib import suppress
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from crackerjack.services.filesystem import FileSystemService
|
|
9
|
+
from crackerjack.services.security import SecurityService
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PublishManagerImpl:
|
|
13
|
+
def __init__(self, console: Console, pkg_path: Path, dry_run: bool = False) -> None:
|
|
14
|
+
self.console = console
|
|
15
|
+
self.pkg_path = pkg_path
|
|
16
|
+
self.dry_run = dry_run
|
|
17
|
+
self.filesystem = FileSystemService()
|
|
18
|
+
self.security = SecurityService()
|
|
19
|
+
|
|
20
|
+
def _run_command(
|
|
21
|
+
self,
|
|
22
|
+
cmd: list[str],
|
|
23
|
+
timeout: int = 300,
|
|
24
|
+
) -> subprocess.CompletedProcess[str]:
|
|
25
|
+
secure_env = self.security.create_secure_command_env()
|
|
26
|
+
|
|
27
|
+
result = subprocess.run(
|
|
28
|
+
cmd,
|
|
29
|
+
check=False,
|
|
30
|
+
cwd=self.pkg_path,
|
|
31
|
+
capture_output=True,
|
|
32
|
+
text=True,
|
|
33
|
+
timeout=timeout,
|
|
34
|
+
env=secure_env,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if result.stdout:
|
|
38
|
+
result.stdout = self.security.mask_tokens(result.stdout)
|
|
39
|
+
if result.stderr:
|
|
40
|
+
result.stderr = self.security.mask_tokens(result.stderr)
|
|
41
|
+
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
def _get_current_version(self) -> str | None:
|
|
45
|
+
pyproject_path = self.pkg_path / "pyproject.toml"
|
|
46
|
+
if not pyproject_path.exists():
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
from tomllib import loads
|
|
50
|
+
|
|
51
|
+
content = self.filesystem.read_file(pyproject_path)
|
|
52
|
+
data = loads(content)
|
|
53
|
+
return data.get("project", {}).get("version")
|
|
54
|
+
except Exception as e:
|
|
55
|
+
self.console.print(f"[yellow]⚠️[/yellow] Error reading version: {e}")
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
def _update_version_in_file(self, new_version: str) -> bool:
|
|
59
|
+
pyproject_path = self.pkg_path / "pyproject.toml"
|
|
60
|
+
try:
|
|
61
|
+
content = self.filesystem.read_file(pyproject_path)
|
|
62
|
+
import re
|
|
63
|
+
|
|
64
|
+
# More specific pattern to only match project version, not tool versions
|
|
65
|
+
pattern = r'^(version\s*=\s*["\'])([^"\']+)(["\'])$'
|
|
66
|
+
replacement = f"\\g<1>{new_version}\\g<3>"
|
|
67
|
+
new_content = re.sub(pattern, replacement, content, flags=re.MULTILINE)
|
|
68
|
+
if content != new_content:
|
|
69
|
+
if not self.dry_run:
|
|
70
|
+
self.filesystem.write_file(pyproject_path, new_content)
|
|
71
|
+
self.console.print(
|
|
72
|
+
f"[green]✅[/green] Updated version to {new_version}",
|
|
73
|
+
)
|
|
74
|
+
return True
|
|
75
|
+
self.console.print(
|
|
76
|
+
"[yellow]⚠️[/yellow] Version pattern not found in pyproject.toml",
|
|
77
|
+
)
|
|
78
|
+
return False
|
|
79
|
+
except Exception as e:
|
|
80
|
+
self.console.print(f"[red]❌[/red] Error updating version: {e}")
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
def _calculate_next_version(self, current: str, bump_type: str) -> str:
|
|
84
|
+
try:
|
|
85
|
+
parts = current.split(".")
|
|
86
|
+
if len(parts) != 3:
|
|
87
|
+
msg = f"Invalid version format: {current}"
|
|
88
|
+
raise ValueError(msg)
|
|
89
|
+
major, minor, patch = map(int, parts)
|
|
90
|
+
if bump_type == "major":
|
|
91
|
+
return f"{major + 1}.0.0"
|
|
92
|
+
if bump_type == "minor":
|
|
93
|
+
return f"{major}.{minor + 1}.0"
|
|
94
|
+
if bump_type == "patch":
|
|
95
|
+
return f"{major}.{minor}.{patch + 1}"
|
|
96
|
+
msg = f"Invalid bump type: {bump_type}"
|
|
97
|
+
raise ValueError(msg)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
self.console.print(f"[red]❌[/red] Error calculating version: {e}")
|
|
100
|
+
raise
|
|
101
|
+
|
|
102
|
+
def bump_version(self, version_type: str) -> str:
|
|
103
|
+
current_version = self._get_current_version()
|
|
104
|
+
if not current_version:
|
|
105
|
+
self.console.print("[red]❌[/red] Could not determine current version")
|
|
106
|
+
msg = "Cannot determine current version"
|
|
107
|
+
raise ValueError(msg)
|
|
108
|
+
self.console.print(f"[cyan]📦[/cyan] Current version: {current_version}")
|
|
109
|
+
|
|
110
|
+
# Handle interactive version selection
|
|
111
|
+
if version_type == "interactive":
|
|
112
|
+
version_type = self._prompt_for_version_type()
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
new_version = self._calculate_next_version(current_version, version_type)
|
|
116
|
+
if self.dry_run:
|
|
117
|
+
self.console.print(
|
|
118
|
+
f"[yellow]🔍[/yellow] Would bump {version_type} version: {current_version} → {new_version}",
|
|
119
|
+
)
|
|
120
|
+
elif self._update_version_in_file(new_version):
|
|
121
|
+
self.console.print(
|
|
122
|
+
f"[green]🚀[/green] Bumped {version_type} version: {current_version} → {new_version}",
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
msg = "Failed to update version in file"
|
|
126
|
+
raise ValueError(msg)
|
|
127
|
+
|
|
128
|
+
return new_version
|
|
129
|
+
except Exception as e:
|
|
130
|
+
self.console.print(f"[red]❌[/red] Version bump failed: {e}")
|
|
131
|
+
raise
|
|
132
|
+
|
|
133
|
+
def _prompt_for_version_type(self) -> str:
|
|
134
|
+
"""Prompt user to select version type interactively."""
|
|
135
|
+
try:
|
|
136
|
+
from rich.prompt import Prompt
|
|
137
|
+
|
|
138
|
+
return Prompt.ask(
|
|
139
|
+
"[cyan]📦[/cyan] Select version bump type",
|
|
140
|
+
choices=["patch", "minor", "major"],
|
|
141
|
+
default="patch",
|
|
142
|
+
)
|
|
143
|
+
except ImportError:
|
|
144
|
+
self.console.print(
|
|
145
|
+
"[yellow]⚠️[/yellow] Rich prompt not available, defaulting to patch"
|
|
146
|
+
)
|
|
147
|
+
return "patch"
|
|
148
|
+
|
|
149
|
+
def validate_auth(self) -> bool:
|
|
150
|
+
auth_methods = self._collect_auth_methods()
|
|
151
|
+
return self._report_auth_status(auth_methods)
|
|
152
|
+
|
|
153
|
+
def _collect_auth_methods(self) -> list[str]:
|
|
154
|
+
auth_methods: list[str] = []
|
|
155
|
+
|
|
156
|
+
env_auth = self._check_env_token_auth()
|
|
157
|
+
if env_auth:
|
|
158
|
+
auth_methods.append(env_auth)
|
|
159
|
+
|
|
160
|
+
keyring_auth = self._check_keyring_auth()
|
|
161
|
+
if keyring_auth:
|
|
162
|
+
auth_methods.append(keyring_auth)
|
|
163
|
+
|
|
164
|
+
return auth_methods
|
|
165
|
+
|
|
166
|
+
def _check_env_token_auth(self) -> str | None:
|
|
167
|
+
import os
|
|
168
|
+
|
|
169
|
+
token = os.getenv("UV_PUBLISH_TOKEN")
|
|
170
|
+
if not token:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
if self.security.validate_token_format(token, "pypi"):
|
|
174
|
+
masked_token = self.security.mask_tokens(token)
|
|
175
|
+
self.console.print(f"[dim]Token format: {masked_token}[/dim]", style="dim")
|
|
176
|
+
return "Environment variable (UV_PUBLISH_TOKEN)"
|
|
177
|
+
self.console.print(
|
|
178
|
+
"[yellow]⚠️[/yellow] UV_PUBLISH_TOKEN format appears invalid",
|
|
179
|
+
)
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
def _check_keyring_auth(self) -> str | None:
|
|
183
|
+
try:
|
|
184
|
+
result = self._run_command(
|
|
185
|
+
["keyring", "get", "https://upload.pypi.org/legacy/", "__token__"],
|
|
186
|
+
)
|
|
187
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
188
|
+
keyring_token = result.stdout.strip()
|
|
189
|
+
if self.security.validate_token_format(keyring_token, "pypi"):
|
|
190
|
+
return "Keyring storage"
|
|
191
|
+
self.console.print(
|
|
192
|
+
"[yellow]⚠️[/yellow] Keyring token format appears invalid",
|
|
193
|
+
)
|
|
194
|
+
except (subprocess.SubprocessError, OSError, FileNotFoundError):
|
|
195
|
+
pass
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
def _report_auth_status(self, auth_methods: list[str]) -> bool:
|
|
199
|
+
if auth_methods:
|
|
200
|
+
self.console.print("[green]✅[/green] PyPI authentication available: ")
|
|
201
|
+
for method in auth_methods:
|
|
202
|
+
self.console.print(f" - {method}")
|
|
203
|
+
return True
|
|
204
|
+
self._display_auth_setup_instructions()
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
def _display_auth_setup_instructions(self) -> None:
|
|
208
|
+
self.console.print("[red]❌[/red] No valid PyPI authentication found")
|
|
209
|
+
self.console.print("\n[yellow]💡[/yellow] Setup options: ")
|
|
210
|
+
self.console.print(
|
|
211
|
+
" 1. Set environment variable: export UV_PUBLISH_TOKEN=<your-pypi-token>",
|
|
212
|
+
)
|
|
213
|
+
self.console.print(
|
|
214
|
+
" 2. Use keyring: keyring set https://upload.pypi.org/legacy/ __token__",
|
|
215
|
+
)
|
|
216
|
+
self.console.print(
|
|
217
|
+
" 3. Ensure token starts with 'pypi-' and is properly formatted",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def build_package(self) -> bool:
|
|
221
|
+
try:
|
|
222
|
+
self.console.print("[yellow]🔨[/yellow] Building package...")
|
|
223
|
+
|
|
224
|
+
if self.dry_run:
|
|
225
|
+
return self._handle_dry_run_build()
|
|
226
|
+
|
|
227
|
+
return self._execute_build()
|
|
228
|
+
except Exception as e:
|
|
229
|
+
self.console.print(f"[red]❌[/red] Build error: {e}")
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
def _handle_dry_run_build(self) -> bool:
|
|
233
|
+
self.console.print("[yellow]🔍[/yellow] Would build package")
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
def _clean_dist_directory(self) -> None:
|
|
237
|
+
"""Clean dist directory to ensure only current version artifacts are uploaded."""
|
|
238
|
+
dist_dir = self.pkg_path / "dist"
|
|
239
|
+
if not dist_dir.exists():
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
import shutil
|
|
244
|
+
|
|
245
|
+
# Remove entire dist directory and recreate it
|
|
246
|
+
shutil.rmtree(dist_dir)
|
|
247
|
+
dist_dir.mkdir(exist_ok=True)
|
|
248
|
+
self.console.print("[cyan]🧹[/cyan] Cleaned dist directory for fresh build")
|
|
249
|
+
except Exception as e:
|
|
250
|
+
self.console.print(
|
|
251
|
+
f"[yellow]⚠️[/yellow] Warning: Could not clean dist directory: {e}"
|
|
252
|
+
)
|
|
253
|
+
# Continue with build anyway - uv publish will fail with clear error
|
|
254
|
+
|
|
255
|
+
def _execute_build(self) -> bool:
|
|
256
|
+
# Clean dist directory before building to avoid uploading multiple versions
|
|
257
|
+
self._clean_dist_directory()
|
|
258
|
+
|
|
259
|
+
result = self._run_command(["uv", "build"])
|
|
260
|
+
|
|
261
|
+
if result.returncode != 0:
|
|
262
|
+
self.console.print(f"[red]❌[/red] Build failed: {result.stderr}")
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
self.console.print("[green]✅[/green] Package built successfully")
|
|
266
|
+
self._display_build_artifacts()
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
def _display_build_artifacts(self) -> None:
|
|
270
|
+
dist_dir = self.pkg_path / "dist"
|
|
271
|
+
if not dist_dir.exists():
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
artifacts = list(dist_dir.glob("*"))
|
|
275
|
+
self.console.print(f"[cyan]📦[/cyan] Build artifacts ({len(artifacts)}): ")
|
|
276
|
+
|
|
277
|
+
for artifact in artifacts[-5:]:
|
|
278
|
+
size_str = self._format_file_size(artifact.stat().st_size)
|
|
279
|
+
self.console.print(f" - {artifact.name} ({size_str})")
|
|
280
|
+
|
|
281
|
+
def _format_file_size(self, size: int) -> str:
|
|
282
|
+
if size < 1024 * 1024:
|
|
283
|
+
return f"{size / 1024:.1f}KB"
|
|
284
|
+
return f"{size / (1024 * 1024):.1f}MB"
|
|
285
|
+
|
|
286
|
+
def publish_package(self) -> bool:
|
|
287
|
+
if not self._validate_prerequisites():
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
self.console.print("[yellow]🚀[/yellow] Publishing to PyPI...")
|
|
292
|
+
return self._perform_publish_workflow()
|
|
293
|
+
except Exception as e:
|
|
294
|
+
self.console.print(f"[red]❌[/red] Publish error: {e}")
|
|
295
|
+
return False
|
|
296
|
+
|
|
297
|
+
def _validate_prerequisites(self) -> bool:
|
|
298
|
+
return self.validate_auth()
|
|
299
|
+
|
|
300
|
+
def _perform_publish_workflow(self) -> bool:
|
|
301
|
+
if self.dry_run:
|
|
302
|
+
return self._handle_dry_run_publish()
|
|
303
|
+
|
|
304
|
+
if not self.build_package():
|
|
305
|
+
return False
|
|
306
|
+
|
|
307
|
+
return self._execute_publish()
|
|
308
|
+
|
|
309
|
+
def _handle_dry_run_publish(self) -> bool:
|
|
310
|
+
self.console.print("[yellow]🔍[/yellow] Would publish package to PyPI")
|
|
311
|
+
return True
|
|
312
|
+
|
|
313
|
+
def _execute_publish(self) -> bool:
|
|
314
|
+
result = self._run_command(["uv", "publish"])
|
|
315
|
+
|
|
316
|
+
if result.returncode != 0:
|
|
317
|
+
self._handle_publish_failure(result.stderr)
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
self._handle_publish_success()
|
|
321
|
+
return True
|
|
322
|
+
|
|
323
|
+
def _handle_publish_failure(self, error_msg: str) -> None:
|
|
324
|
+
self.console.print(f"[red]❌[/red] Publish failed: {error_msg}")
|
|
325
|
+
|
|
326
|
+
def _handle_publish_success(self) -> None:
|
|
327
|
+
self.console.print("[green]🎉[/green] Package published successfully!")
|
|
328
|
+
self._display_package_url()
|
|
329
|
+
|
|
330
|
+
def _display_package_url(self) -> None:
|
|
331
|
+
current_version = self._get_current_version()
|
|
332
|
+
package_name = self._get_package_name()
|
|
333
|
+
|
|
334
|
+
if package_name and current_version:
|
|
335
|
+
url = f"https://pypi.org/project/{package_name}/{current_version}/"
|
|
336
|
+
self.console.print(f"[cyan]🔗[/cyan] Package URL: {url}")
|
|
337
|
+
|
|
338
|
+
def _get_package_name(self) -> str | None:
|
|
339
|
+
pyproject_path = self.pkg_path / "pyproject.toml"
|
|
340
|
+
|
|
341
|
+
with suppress(Exception):
|
|
342
|
+
from tomllib import loads
|
|
343
|
+
|
|
344
|
+
content = self.filesystem.read_file(pyproject_path)
|
|
345
|
+
data = loads(content)
|
|
346
|
+
return data.get("project", {}).get("name", "")
|
|
347
|
+
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
def cleanup_old_releases(self, keep_releases: int = 10) -> bool:
|
|
351
|
+
try:
|
|
352
|
+
self.console.print(
|
|
353
|
+
f"[yellow]🧹[/yellow] Cleaning up old releases (keeping {keep_releases})...",
|
|
354
|
+
)
|
|
355
|
+
if self.dry_run:
|
|
356
|
+
self.console.print(
|
|
357
|
+
"[yellow]🔍[/yellow] Would clean up old PyPI releases",
|
|
358
|
+
)
|
|
359
|
+
return True
|
|
360
|
+
pyproject_path = self.pkg_path / "pyproject.toml"
|
|
361
|
+
from tomllib import loads
|
|
362
|
+
|
|
363
|
+
content = self.filesystem.read_file(pyproject_path)
|
|
364
|
+
data = loads(content)
|
|
365
|
+
package_name = data.get("project", {}).get("name", "")
|
|
366
|
+
if not package_name:
|
|
367
|
+
self.console.print(
|
|
368
|
+
"[yellow]⚠️[/yellow] Could not determine package name",
|
|
369
|
+
)
|
|
370
|
+
return False
|
|
371
|
+
self.console.print(
|
|
372
|
+
f"[cyan]📦[/cyan] Would analyze releases for {package_name}",
|
|
373
|
+
)
|
|
374
|
+
self.console.print(
|
|
375
|
+
f"[cyan]🔧[/cyan] Would keep {keep_releases} most recent releases",
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
return True
|
|
379
|
+
except Exception as e:
|
|
380
|
+
self.console.print(f"[red]❌[/red] Cleanup error: {e}")
|
|
381
|
+
return False
|
|
382
|
+
|
|
383
|
+
def create_git_tag(self, version: str) -> bool:
|
|
384
|
+
try:
|
|
385
|
+
if self.dry_run:
|
|
386
|
+
self.console.print(
|
|
387
|
+
f"[yellow]🔍[/yellow] Would create git tag: v{version}",
|
|
388
|
+
)
|
|
389
|
+
return True
|
|
390
|
+
result = self._run_command(["git", "tag", f"v{version}"])
|
|
391
|
+
if result.returncode == 0:
|
|
392
|
+
self.console.print(f"[green]🏷️[/green] Created git tag: v{version}")
|
|
393
|
+
push_result = self._run_command(
|
|
394
|
+
["git", "push", "origin", f"v{version}"],
|
|
395
|
+
)
|
|
396
|
+
if push_result.returncode == 0:
|
|
397
|
+
self.console.print("[green]📤[/green] Pushed tag to remote")
|
|
398
|
+
else:
|
|
399
|
+
self.console.print(
|
|
400
|
+
f"[yellow]⚠️[/yellow] Tag created but push failed: {push_result.stderr}",
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
return True
|
|
404
|
+
self.console.print(
|
|
405
|
+
f"[red]❌[/red] Failed to create tag: {result.stderr}",
|
|
406
|
+
)
|
|
407
|
+
return False
|
|
408
|
+
except Exception as e:
|
|
409
|
+
self.console.print(f"[red]❌[/red] Tag creation error: {e}")
|
|
410
|
+
return False
|
|
411
|
+
|
|
412
|
+
def get_package_info(self) -> dict[str, t.Any]:
|
|
413
|
+
pyproject_path = self.pkg_path / "pyproject.toml"
|
|
414
|
+
if not pyproject_path.exists():
|
|
415
|
+
return {}
|
|
416
|
+
try:
|
|
417
|
+
from tomllib import loads
|
|
418
|
+
|
|
419
|
+
content = self.filesystem.read_file(pyproject_path)
|
|
420
|
+
data = loads(content)
|
|
421
|
+
project = data.get("project", {})
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
"name": project.get("name", ""),
|
|
425
|
+
"version": project.get("version", ""),
|
|
426
|
+
"description": project.get("description", ""),
|
|
427
|
+
"authors": project.get("authors", []),
|
|
428
|
+
"dependencies": project.get("dependencies", []),
|
|
429
|
+
"python_requires": project.get("requires-python", ""),
|
|
430
|
+
}
|
|
431
|
+
except Exception as e:
|
|
432
|
+
self.console.print(f"[yellow]⚠️[/yellow] Error reading package info: {e}")
|
|
433
|
+
return {}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Test command building and configuration.
|
|
2
|
+
|
|
3
|
+
This module handles pytest command construction with various options and configurations.
|
|
4
|
+
Split from test_manager.py for better separation of concerns.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from crackerjack.models.protocols import OptionsProtocol
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestCommandBuilder:
|
|
13
|
+
"""Builds pytest commands with appropriate options and configurations."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, pkg_path: Path) -> None:
|
|
16
|
+
self.pkg_path = pkg_path
|
|
17
|
+
|
|
18
|
+
def build_command(self, options: OptionsProtocol) -> list[str]:
|
|
19
|
+
"""Build complete pytest command with all options."""
|
|
20
|
+
cmd = ["python", "-m", "pytest"]
|
|
21
|
+
|
|
22
|
+
self._add_coverage_options(cmd, options)
|
|
23
|
+
self._add_worker_options(cmd, options)
|
|
24
|
+
self._add_benchmark_options(cmd, options)
|
|
25
|
+
self._add_timeout_options(cmd, options)
|
|
26
|
+
self._add_verbosity_options(cmd, options)
|
|
27
|
+
self._add_test_path(cmd)
|
|
28
|
+
|
|
29
|
+
return cmd
|
|
30
|
+
|
|
31
|
+
def get_optimal_workers(self, options: OptionsProtocol) -> int:
|
|
32
|
+
"""Calculate optimal number of pytest workers based on system and configuration."""
|
|
33
|
+
if hasattr(options, "test_workers") and options.test_workers:
|
|
34
|
+
return options.test_workers
|
|
35
|
+
|
|
36
|
+
# Auto-detect based on CPU count
|
|
37
|
+
import multiprocessing
|
|
38
|
+
|
|
39
|
+
cpu_count = multiprocessing.cpu_count()
|
|
40
|
+
|
|
41
|
+
# Conservative worker count to avoid overwhelming the system
|
|
42
|
+
if cpu_count <= 2:
|
|
43
|
+
return 1
|
|
44
|
+
elif cpu_count <= 4:
|
|
45
|
+
return 2
|
|
46
|
+
elif cpu_count <= 8:
|
|
47
|
+
return 3
|
|
48
|
+
return 4
|
|
49
|
+
|
|
50
|
+
def get_test_timeout(self, options: OptionsProtocol) -> int:
|
|
51
|
+
"""Get test timeout based on options or default."""
|
|
52
|
+
if hasattr(options, "test_timeout") and options.test_timeout:
|
|
53
|
+
return options.test_timeout
|
|
54
|
+
|
|
55
|
+
# Default timeout based on test configuration
|
|
56
|
+
if hasattr(options, "benchmark") and options.benchmark:
|
|
57
|
+
return 900 # 15 minutes for benchmarks
|
|
58
|
+
return 300 # 5 minutes for regular tests
|
|
59
|
+
|
|
60
|
+
def _add_coverage_options(self, cmd: list[str], options: OptionsProtocol) -> None:
|
|
61
|
+
"""Add coverage-related options to command."""
|
|
62
|
+
# Always include coverage for comprehensive testing
|
|
63
|
+
cmd.extend(
|
|
64
|
+
[
|
|
65
|
+
"--cov=crackerjack",
|
|
66
|
+
"--cov-report=term-missing",
|
|
67
|
+
"--cov-report=html",
|
|
68
|
+
"--cov-fail-under=0", # Don't fail on low coverage, let ratchet handle it
|
|
69
|
+
]
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def _add_worker_options(self, cmd: list[str], options: OptionsProtocol) -> None:
|
|
73
|
+
"""Add parallel execution options to command."""
|
|
74
|
+
workers = self.get_optimal_workers(options)
|
|
75
|
+
if workers > 1:
|
|
76
|
+
cmd.extend(["-n", str(workers)])
|
|
77
|
+
|
|
78
|
+
def _add_benchmark_options(self, cmd: list[str], options: OptionsProtocol) -> None:
|
|
79
|
+
"""Add benchmark-specific options to command."""
|
|
80
|
+
if hasattr(options, "benchmark") and options.benchmark:
|
|
81
|
+
cmd.extend(
|
|
82
|
+
[
|
|
83
|
+
"--benchmark-only",
|
|
84
|
+
"--benchmark-sort=mean",
|
|
85
|
+
"--benchmark-columns=min,max,mean,stddev",
|
|
86
|
+
]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def _add_timeout_options(self, cmd: list[str], options: OptionsProtocol) -> None:
|
|
90
|
+
"""Add timeout options to command."""
|
|
91
|
+
timeout = self.get_test_timeout(options)
|
|
92
|
+
cmd.extend(["--timeout", str(timeout)])
|
|
93
|
+
|
|
94
|
+
def _add_verbosity_options(self, cmd: list[str], options: OptionsProtocol) -> None:
|
|
95
|
+
"""Add verbosity and output formatting options."""
|
|
96
|
+
# Always use verbose output for better progress tracking
|
|
97
|
+
cmd.append("-v")
|
|
98
|
+
|
|
99
|
+
# Add useful output options
|
|
100
|
+
cmd.extend(
|
|
101
|
+
[
|
|
102
|
+
"--tb=short", # Shorter traceback format
|
|
103
|
+
"--strict-markers", # Ensure all markers are defined
|
|
104
|
+
"--strict-config", # Ensure configuration is valid
|
|
105
|
+
]
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def _add_test_path(self, cmd: list[str]) -> None:
|
|
109
|
+
"""Add test path to command."""
|
|
110
|
+
# Add tests directory if it exists, otherwise current directory
|
|
111
|
+
test_paths = ["tests", "test"]
|
|
112
|
+
|
|
113
|
+
for test_path in test_paths:
|
|
114
|
+
full_path = self.pkg_path / test_path
|
|
115
|
+
if full_path.exists() and full_path.is_dir():
|
|
116
|
+
cmd.append(str(full_path))
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
# Fallback to current directory
|
|
120
|
+
cmd.append(str(self.pkg_path))
|
|
121
|
+
|
|
122
|
+
def build_specific_test_command(self, test_pattern: str) -> list[str]:
|
|
123
|
+
"""Build command for running specific tests matching a pattern."""
|
|
124
|
+
cmd = ["python", "-m", "pytest", "-v"]
|
|
125
|
+
|
|
126
|
+
# Add basic coverage
|
|
127
|
+
cmd.extend(
|
|
128
|
+
[
|
|
129
|
+
"--cov=crackerjack",
|
|
130
|
+
"--cov-report=term-missing",
|
|
131
|
+
]
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Add the test pattern
|
|
135
|
+
cmd.extend(["-k", test_pattern])
|
|
136
|
+
|
|
137
|
+
# Add test path
|
|
138
|
+
self._add_test_path(cmd)
|
|
139
|
+
|
|
140
|
+
return cmd
|
|
141
|
+
|
|
142
|
+
def build_validation_command(self) -> list[str]:
|
|
143
|
+
"""Build command for test environment validation."""
|
|
144
|
+
return [
|
|
145
|
+
"python",
|
|
146
|
+
"-m",
|
|
147
|
+
"pytest",
|
|
148
|
+
"--collect-only",
|
|
149
|
+
"--quiet",
|
|
150
|
+
"tests" if (self.pkg_path / "tests").exists() else ".",
|
|
151
|
+
]
|