pipu-cli 0.1.dev6__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/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
- # Time-to-live for editable packages cache (seconds)
32
- EDITABLE_PACKAGES_CACHE_TTL = float(os.environ.get('PIPU_CACHE_TTL', '60.0'))
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
- # Scroll buffer size for log output
55
- LOG_SCROLL_BUFFER_LINES = int(os.environ.get('PIPU_LOG_BUFFER', '1000'))
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
- # Delay for table refresh operations (seconds)
58
- TABLE_REFRESH_DELAY = float(os.environ.get('PIPU_TABLE_REFRESH', '0.01'))
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
@@ -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)