pipu-cli 0.1.dev7__py3-none-any.whl → 0.2.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.
- pipu_cli/__init__.py +2 -2
- pipu_cli/cache.py +316 -0
- pipu_cli/cli.py +863 -813
- pipu_cli/config.py +7 -58
- pipu_cli/config_file.py +80 -0
- pipu_cli/output.py +99 -0
- pipu_cli/package_management.py +1145 -0
- pipu_cli/pretty.py +286 -0
- pipu_cli/requirements.py +100 -0
- pipu_cli/rollback.py +110 -0
- pipu_cli-0.2.0.dist-info/METADATA +422 -0
- pipu_cli-0.2.0.dist-info/RECORD +16 -0
- pipu_cli/common.py +0 -4
- pipu_cli/internals.py +0 -815
- pipu_cli/package_constraints.py +0 -2296
- pipu_cli/thread_safe.py +0 -243
- pipu_cli/ui/__init__.py +0 -51
- pipu_cli/ui/apps.py +0 -1464
- pipu_cli/ui/constants.py +0 -33
- pipu_cli/ui/modal_dialogs.py +0 -1375
- pipu_cli/ui/table_widgets.py +0 -344
- pipu_cli/utils.py +0 -169
- pipu_cli-0.1.dev7.dist-info/METADATA +0 -517
- pipu_cli-0.1.dev7.dist-info/RECORD +0 -19
- {pipu_cli-0.1.dev7.dist-info → pipu_cli-0.2.0.dist-info}/WHEEL +0 -0
- {pipu_cli-0.1.dev7.dist-info → pipu_cli-0.2.0.dist-info}/entry_points.txt +0 -0
- {pipu_cli-0.1.dev7.dist-info → pipu_cli-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {pipu_cli-0.1.dev7.dist-info → pipu_cli-0.2.0.dist-info}/top_level.txt +0 -0
pipu_cli/config.py
CHANGED
|
@@ -7,6 +7,7 @@ environment-based settings to improve maintainability.
|
|
|
7
7
|
|
|
8
8
|
import os
|
|
9
9
|
import logging
|
|
10
|
+
from pathlib import Path
|
|
10
11
|
|
|
11
12
|
# ============================================================================
|
|
12
13
|
# Network Configuration
|
|
@@ -15,47 +16,18 @@ import logging
|
|
|
15
16
|
# Default timeout for network operations (seconds)
|
|
16
17
|
DEFAULT_NETWORK_TIMEOUT = int(os.environ.get('PIPU_TIMEOUT', '10'))
|
|
17
18
|
|
|
18
|
-
# Number of retries for failed network operations
|
|
19
|
-
DEFAULT_NETWORK_RETRIES = int(os.environ.get('PIPU_RETRIES', '0'))
|
|
20
|
-
|
|
21
|
-
# Maximum consecutive network errors before failing
|
|
22
|
-
MAX_CONSECUTIVE_NETWORK_ERRORS = int(os.environ.get('PIPU_MAX_NETWORK_ERRORS', '1'))
|
|
23
|
-
|
|
24
|
-
# Brief delay between retries (seconds)
|
|
25
|
-
RETRY_DELAY = float(os.environ.get('PIPU_RETRY_DELAY', '0.5'))
|
|
26
|
-
|
|
27
19
|
# ============================================================================
|
|
28
20
|
# Cache Configuration
|
|
29
21
|
# ============================================================================
|
|
30
22
|
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# ============================================================================
|
|
35
|
-
# Subprocess Configuration
|
|
36
|
-
# ============================================================================
|
|
37
|
-
|
|
38
|
-
# Timeout for subprocess operations (seconds)
|
|
39
|
-
SUBPROCESS_TIMEOUT = int(os.environ.get('PIPU_SUBPROCESS_TIMEOUT', '30'))
|
|
40
|
-
|
|
41
|
-
# Timeout for package uninstall operations (seconds)
|
|
42
|
-
UNINSTALL_TIMEOUT = int(os.environ.get('PIPU_UNINSTALL_TIMEOUT', '120'))
|
|
43
|
-
|
|
44
|
-
# Timeout for forced process termination (seconds)
|
|
45
|
-
FORCE_KILL_TIMEOUT = float(os.environ.get('PIPU_FORCE_KILL_TIMEOUT', '5.0'))
|
|
46
|
-
|
|
47
|
-
# ============================================================================
|
|
48
|
-
# TUI Configuration
|
|
49
|
-
# ============================================================================
|
|
50
|
-
|
|
51
|
-
# Timeout for graceful exit (seconds)
|
|
52
|
-
FORCE_EXIT_TIMEOUT = float(os.environ.get('PIPU_EXIT_TIMEOUT', '3.0'))
|
|
23
|
+
# Default cache TTL in seconds (1 hour)
|
|
24
|
+
DEFAULT_CACHE_TTL = int(os.environ.get('PIPU_CACHE_TTL', '3600'))
|
|
53
25
|
|
|
54
|
-
#
|
|
55
|
-
|
|
26
|
+
# Whether caching is enabled by default
|
|
27
|
+
DEFAULT_CACHE_ENABLED = os.environ.get('PIPU_CACHE_ENABLED', 'true').lower() in ('true', '1', 'yes')
|
|
56
28
|
|
|
57
|
-
#
|
|
58
|
-
|
|
29
|
+
# Base directory for cache storage
|
|
30
|
+
CACHE_BASE_DIR = Path(os.environ.get('PIPU_CACHE_DIR', str(Path.home() / ".pipu" / "cache")))
|
|
59
31
|
|
|
60
32
|
# ============================================================================
|
|
61
33
|
# Logging Configuration
|
|
@@ -70,26 +42,3 @@ try:
|
|
|
70
42
|
except AttributeError:
|
|
71
43
|
LOG_LEVEL = logging.WARNING
|
|
72
44
|
|
|
73
|
-
# ============================================================================
|
|
74
|
-
# Testing Configuration
|
|
75
|
-
# ============================================================================
|
|
76
|
-
|
|
77
|
-
# Skip package validation in tests (set by test fixtures)
|
|
78
|
-
SKIP_PACKAGE_VALIDATION = os.environ.get('PIPU_SKIP_PKG_VALIDATION', '').lower() in ('1', 'true', 'yes')
|
|
79
|
-
|
|
80
|
-
# ============================================================================
|
|
81
|
-
# File Paths
|
|
82
|
-
# ============================================================================
|
|
83
|
-
|
|
84
|
-
# Default constraints file name
|
|
85
|
-
DEFAULT_CONSTRAINTS_FILE = 'constraints.txt'
|
|
86
|
-
|
|
87
|
-
# Default ignores file name
|
|
88
|
-
DEFAULT_IGNORES_FILE = 'ignores.txt'
|
|
89
|
-
|
|
90
|
-
# ============================================================================
|
|
91
|
-
# Version Display
|
|
92
|
-
# ============================================================================
|
|
93
|
-
|
|
94
|
-
# Maximum packages to show in summary before truncating
|
|
95
|
-
MAX_PACKAGES_DISPLAY = 5
|
pipu_cli/config_file.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Configuration file support for pipu."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, Any, Optional
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import tomllib
|
|
9
|
+
except ImportError:
|
|
10
|
+
import tomli as tomllib # type: ignore
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def find_config_file() -> Optional[Path]:
|
|
14
|
+
"""Find the pipu configuration file.
|
|
15
|
+
|
|
16
|
+
Searches in order:
|
|
17
|
+
1. .pipu.toml in current directory
|
|
18
|
+
2. pyproject.toml in current directory (looks for [tool.pipu] section)
|
|
19
|
+
3. ~/.config/pipu/config.toml
|
|
20
|
+
|
|
21
|
+
:returns: Path to config file or None if not found
|
|
22
|
+
"""
|
|
23
|
+
# Check current directory for .pipu.toml
|
|
24
|
+
local_config = Path(".pipu.toml")
|
|
25
|
+
if local_config.exists():
|
|
26
|
+
return local_config
|
|
27
|
+
|
|
28
|
+
# Check pyproject.toml for [tool.pipu] section
|
|
29
|
+
pyproject = Path("pyproject.toml")
|
|
30
|
+
if pyproject.exists():
|
|
31
|
+
try:
|
|
32
|
+
with open(pyproject, "rb") as f:
|
|
33
|
+
data = tomllib.load(f)
|
|
34
|
+
if "tool" in data and "pipu" in data["tool"]:
|
|
35
|
+
return pyproject
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
# Check user config directory
|
|
40
|
+
user_config = Path.home() / ".config" / "pipu" / "config.toml"
|
|
41
|
+
if user_config.exists():
|
|
42
|
+
return user_config
|
|
43
|
+
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def load_config(config_path: Optional[Path] = None) -> Dict[str, Any]:
|
|
48
|
+
"""Load pipu configuration from file.
|
|
49
|
+
|
|
50
|
+
:param config_path: Optional explicit path to config file
|
|
51
|
+
:returns: Configuration dictionary
|
|
52
|
+
"""
|
|
53
|
+
if config_path is None:
|
|
54
|
+
config_path = find_config_file()
|
|
55
|
+
|
|
56
|
+
if config_path is None:
|
|
57
|
+
return {}
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
with open(config_path, "rb") as f:
|
|
61
|
+
data = tomllib.load(f)
|
|
62
|
+
|
|
63
|
+
# Extract [tool.pipu] section from pyproject.toml
|
|
64
|
+
if config_path.name == "pyproject.toml":
|
|
65
|
+
return data.get("tool", {}).get("pipu", {})
|
|
66
|
+
|
|
67
|
+
return data
|
|
68
|
+
except Exception:
|
|
69
|
+
return {}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_config_value(config: Dict[str, Any], key: str, default: Any = None) -> Any:
|
|
73
|
+
"""Get a configuration value with default.
|
|
74
|
+
|
|
75
|
+
:param config: Configuration dictionary
|
|
76
|
+
:param key: Key to look up
|
|
77
|
+
:param default: Default value if key not found
|
|
78
|
+
:returns: Configuration value or default
|
|
79
|
+
"""
|
|
80
|
+
return config.get(key, default)
|
pipu_cli/output.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Output formatting for pipu CLI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import List, Optional, Any, Dict
|
|
5
|
+
from dataclasses import asdict
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from pipu_cli.package_management import UpgradePackageInfo, UpgradedPackage, BlockedPackageInfo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OutputFormatter:
|
|
13
|
+
"""Base class for output formatting."""
|
|
14
|
+
|
|
15
|
+
def format_upgradable(self, packages: List[UpgradePackageInfo]) -> str:
|
|
16
|
+
"""Format upgradable packages."""
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
def format_blocked(self, packages: List[BlockedPackageInfo]) -> str:
|
|
20
|
+
"""Format blocked packages."""
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
|
|
23
|
+
def format_results(self, results: List[UpgradedPackage]) -> str:
|
|
24
|
+
"""Format upgrade results."""
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class JsonOutputFormatter(OutputFormatter):
|
|
29
|
+
"""JSON output formatter."""
|
|
30
|
+
|
|
31
|
+
def _package_to_dict(self, pkg: Any) -> Dict[str, Any]:
|
|
32
|
+
"""Convert a package dataclass to a JSON-serializable dict."""
|
|
33
|
+
result = {}
|
|
34
|
+
for field_name in pkg.__dataclass_fields__:
|
|
35
|
+
value = getattr(pkg, field_name)
|
|
36
|
+
if hasattr(value, '__str__'):
|
|
37
|
+
result[field_name] = str(value)
|
|
38
|
+
else:
|
|
39
|
+
result[field_name] = value
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
def format_upgradable(self, packages: List[UpgradePackageInfo]) -> str:
|
|
43
|
+
"""Format upgradable packages as JSON."""
|
|
44
|
+
data = {
|
|
45
|
+
"upgradable": [self._package_to_dict(pkg) for pkg in packages],
|
|
46
|
+
"count": len(packages)
|
|
47
|
+
}
|
|
48
|
+
return json.dumps(data, indent=2)
|
|
49
|
+
|
|
50
|
+
def format_blocked(self, packages: List[BlockedPackageInfo]) -> str:
|
|
51
|
+
"""Format blocked packages as JSON."""
|
|
52
|
+
data = {
|
|
53
|
+
"blocked": [self._package_to_dict(pkg) for pkg in packages],
|
|
54
|
+
"count": len(packages)
|
|
55
|
+
}
|
|
56
|
+
return json.dumps(data, indent=2)
|
|
57
|
+
|
|
58
|
+
def format_results(self, results: List[UpgradedPackage]) -> str:
|
|
59
|
+
"""Format upgrade results as JSON."""
|
|
60
|
+
successful = [self._package_to_dict(pkg) for pkg in results if pkg.upgraded]
|
|
61
|
+
failed = [self._package_to_dict(pkg) for pkg in results if not pkg.upgraded]
|
|
62
|
+
|
|
63
|
+
data = {
|
|
64
|
+
"successful": successful,
|
|
65
|
+
"failed": failed,
|
|
66
|
+
"total": len(results),
|
|
67
|
+
"success_count": len(successful),
|
|
68
|
+
"failure_count": len(failed)
|
|
69
|
+
}
|
|
70
|
+
return json.dumps(data, indent=2)
|
|
71
|
+
|
|
72
|
+
def format_all(
|
|
73
|
+
self,
|
|
74
|
+
upgradable: List[UpgradePackageInfo],
|
|
75
|
+
blocked: Optional[List[BlockedPackageInfo]] = None,
|
|
76
|
+
results: Optional[List[UpgradedPackage]] = None
|
|
77
|
+
) -> str:
|
|
78
|
+
"""Format all data as a single JSON object."""
|
|
79
|
+
data = {
|
|
80
|
+
"upgradable": [self._package_to_dict(pkg) for pkg in upgradable],
|
|
81
|
+
"upgradable_count": len(upgradable)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if blocked is not None:
|
|
85
|
+
data["blocked"] = [self._package_to_dict(pkg) for pkg in blocked]
|
|
86
|
+
data["blocked_count"] = len(blocked)
|
|
87
|
+
|
|
88
|
+
if results is not None:
|
|
89
|
+
successful = [self._package_to_dict(pkg) for pkg in results if pkg.upgraded]
|
|
90
|
+
failed = [self._package_to_dict(pkg) for pkg in results if not pkg.upgraded]
|
|
91
|
+
data["results"] = {
|
|
92
|
+
"successful": successful,
|
|
93
|
+
"failed": failed,
|
|
94
|
+
"total": len(results),
|
|
95
|
+
"success_count": len(successful),
|
|
96
|
+
"failure_count": len(failed)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return json.dumps(data, indent=2)
|