pipu-cli 0.1.dev7__py3-none-any.whl → 0.2.1__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,79 @@
1
+ """Configuration file support for pipu."""
2
+
3
+ from pathlib import Path
4
+ from typing import Dict, Any, Optional
5
+
6
+ try:
7
+ import tomllib
8
+ except ImportError:
9
+ import tomli as tomllib # type: ignore
10
+
11
+
12
+ def find_config_file() -> Optional[Path]:
13
+ """Find the pipu configuration file.
14
+
15
+ Searches in order:
16
+ 1. .pipu.toml in current directory
17
+ 2. pyproject.toml in current directory (looks for [tool.pipu] section)
18
+ 3. ~/.config/pipu/config.toml
19
+
20
+ :returns: Path to config file or None if not found
21
+ """
22
+ # Check current directory for .pipu.toml
23
+ local_config = Path(".pipu.toml")
24
+ if local_config.exists():
25
+ return local_config
26
+
27
+ # Check pyproject.toml for [tool.pipu] section
28
+ pyproject = Path("pyproject.toml")
29
+ if pyproject.exists():
30
+ try:
31
+ with open(pyproject, "rb") as f:
32
+ data = tomllib.load(f)
33
+ if "tool" in data and "pipu" in data["tool"]:
34
+ return pyproject
35
+ except Exception:
36
+ pass
37
+
38
+ # Check user config directory
39
+ user_config = Path.home() / ".config" / "pipu" / "config.toml"
40
+ if user_config.exists():
41
+ return user_config
42
+
43
+ return None
44
+
45
+
46
+ def load_config(config_path: Optional[Path] = None) -> Dict[str, Any]:
47
+ """Load pipu configuration from file.
48
+
49
+ :param config_path: Optional explicit path to config file
50
+ :returns: Configuration dictionary
51
+ """
52
+ if config_path is None:
53
+ config_path = find_config_file()
54
+
55
+ if config_path is None:
56
+ return {}
57
+
58
+ try:
59
+ with open(config_path, "rb") as f:
60
+ data = tomllib.load(f)
61
+
62
+ # Extract [tool.pipu] section from pyproject.toml
63
+ if config_path.name == "pyproject.toml":
64
+ return data.get("tool", {}).get("pipu", {})
65
+
66
+ return data
67
+ except Exception:
68
+ return {}
69
+
70
+
71
+ def get_config_value(config: Dict[str, Any], key: str, default: Any = None) -> Any:
72
+ """Get a configuration value with default.
73
+
74
+ :param config: Configuration dictionary
75
+ :param key: Key to look up
76
+ :param default: Default value if key not found
77
+ :returns: Configuration value or default
78
+ """
79
+ 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)