pkgwise 0.1.0__tar.gz

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.
Files changed (40) hide show
  1. pkgwise-0.1.0/LICENSE +21 -0
  2. pkgwise-0.1.0/PKG-INFO +135 -0
  3. pkgwise-0.1.0/README.md +100 -0
  4. pkgwise-0.1.0/pkgwise/__init__.py +12 -0
  5. pkgwise-0.1.0/pkgwise/__main__.py +6 -0
  6. pkgwise-0.1.0/pkgwise/backends/__init__.py +1 -0
  7. pkgwise-0.1.0/pkgwise/backends/arch.py +223 -0
  8. pkgwise-0.1.0/pkgwise/backends/base.py +107 -0
  9. pkgwise-0.1.0/pkgwise/backends/debian.py +247 -0
  10. pkgwise-0.1.0/pkgwise/backends/factory.py +63 -0
  11. pkgwise-0.1.0/pkgwise/cli.py +222 -0
  12. pkgwise-0.1.0/pkgwise/core/__init__.py +1 -0
  13. pkgwise-0.1.0/pkgwise/core/analyzer.py +169 -0
  14. pkgwise-0.1.0/pkgwise/core/models.py +126 -0
  15. pkgwise-0.1.0/pkgwise/core/priority.py +199 -0
  16. pkgwise-0.1.0/pkgwise/core/storage.py +82 -0
  17. pkgwise-0.1.0/pkgwise/mockdata.py +43 -0
  18. pkgwise-0.1.0/pkgwise/py.typed +0 -0
  19. pkgwise-0.1.0/pkgwise/ui/__init__.py +1 -0
  20. pkgwise-0.1.0/pkgwise/ui/app.py +228 -0
  21. pkgwise-0.1.0/pkgwise/ui/screens.py +406 -0
  22. pkgwise-0.1.0/pkgwise/ui/widgets.py +57 -0
  23. pkgwise-0.1.0/pkgwise/utils/__init__.py +1 -0
  24. pkgwise-0.1.0/pkgwise/utils/commands.py +98 -0
  25. pkgwise-0.1.0/pkgwise/utils/config.py +134 -0
  26. pkgwise-0.1.0/pkgwise/utils/distro.py +88 -0
  27. pkgwise-0.1.0/pkgwise/utils/formatting.py +58 -0
  28. pkgwise-0.1.0/pkgwise.egg-info/PKG-INFO +135 -0
  29. pkgwise-0.1.0/pkgwise.egg-info/SOURCES.txt +38 -0
  30. pkgwise-0.1.0/pkgwise.egg-info/dependency_links.txt +1 -0
  31. pkgwise-0.1.0/pkgwise.egg-info/entry_points.txt +2 -0
  32. pkgwise-0.1.0/pkgwise.egg-info/requires.txt +9 -0
  33. pkgwise-0.1.0/pkgwise.egg-info/top_level.txt +1 -0
  34. pkgwise-0.1.0/pyproject.toml +84 -0
  35. pkgwise-0.1.0/setup.cfg +4 -0
  36. pkgwise-0.1.0/tests/test_analyzer.py +133 -0
  37. pkgwise-0.1.0/tests/test_backends.py +239 -0
  38. pkgwise-0.1.0/tests/test_priority.py +89 -0
  39. pkgwise-0.1.0/tests/test_storage.py +120 -0
  40. pkgwise-0.1.0/tests/test_tui.py +87 -0
pkgwise-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PkgWise Developers
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
pkgwise-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: pkgwise
3
+ Version: 0.1.0
4
+ Summary: Know what you're updating before you update — a smart Linux update analyzer.
5
+ Author: PkgWise Developers
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Senpi-Station/PkgWise
8
+ Project-URL: Repository, https://github.com/Senpi-Station/PkgWise
9
+ Project-URL: Issues, https://github.com/Senpi-Station/PkgWise/issues
10
+ Project-URL: Changelog, https://github.com/Senpi-Station/PkgWise/blob/main/CHANGELOG.md
11
+ Keywords: linux,packages,updates,pacman,apt,tui,system-administration
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: System :: Systems Administration
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: psutil<7,>=5.9
29
+ Requires-Dist: textual<2,>=0.60
30
+ Provides-Extra: test
31
+ Requires-Dist: pytest>=7; extra == "test"
32
+ Requires-Dist: pytest-cov>=4; extra == "test"
33
+ Requires-Dist: pytest-asyncio>=0.23; python_version >= "3.9" and extra == "test"
34
+ Dynamic: license-file
35
+
36
+ <h1 align="center">PkgWise</h1>
37
+
38
+ <p align="center">
39
+ <b>Know what you're updating before you update.</b><br/>
40
+ A conservative Linux package-update analyzer for Arch (pacman) and Debian-family (apt) systems.
41
+ </p>
42
+
43
+ <p align="center">
44
+ <a href="https://github.com/Senpi-Station/PkgWise/actions"><img src="https://github.com/Senpi-Station/PkgWise/actions/workflows/ci.yaml/badge.svg" alt="CI"></a>
45
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
46
+ <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.9%2B-blue" alt="Python 3.9+"></a>
47
+ </p>
48
+
49
+ PkgWise checks for available updates **without installing anything**, shows you what
50
+ is waiting (name, versions, sizes, security and reboot hints), scores each update with
51
+ a **transparent, conservative priority**, and only ever runs a real update after you
52
+ explicitly confirm the exact command.
53
+
54
+ ## Features
55
+
56
+ - **Dual package managers** — `pacman` (Arch) and `apt-get`/`apt-cache` (Debian, Kali, Ubuntu).
57
+ - **Sizes you can plan with** — download and installed sizes are read from the package
58
+ manager's own metadata, never guessed.
59
+ - **Transparent priorities** — every CRITICAL/HIGH/MEDIUM/LOW label comes with the
60
+ human-readable reasons behind it. Unknown facts are shown as “Unknown”, never invented.
61
+ - **Security-first updates** — nothing runs automatically, nothing is removed or cleaned,
62
+ empty selections can never trigger a full-system upgrade, and disk-space + privilege
63
+ checks gate every real update.
64
+ - **Two interfaces** — a fast terminal CLI and a Textual TUI with selection, detail and
65
+ confirmation screens.
66
+ - **Mock mode** — preview the whole tool on any machine without touching a real package
67
+ manager.
68
+
69
+ ## Installation
70
+
71
+ ```bash
72
+ pip install pkgwise # core (storage + CLI)
73
+ pip install "pkgwise[all]" # + TUI (textual) and enhanced storage stats (psutil)
74
+ ```
75
+
76
+ Requires Python 3.9+. Works on Arch-based and Debian-based Linux systems.
77
+
78
+ ## Quick start
79
+
80
+ ```console
81
+ $ pkgwise check # storage summary + available updates, prioritized
82
+ $ pkgwise updates # just the pending updates
83
+ $ pkgwise storage # disk information only (no package manager needed)
84
+ $ pkgwise --refresh # refresh package indexes first (network)
85
+ $ pkgwise # launch the interactive TUI
86
+ $ pkgwise --version # version and system info
87
+ ```
88
+
89
+ ## Example output
90
+
91
+ ```
92
+ STORAGE
93
+ Total : 291.4 GB
94
+ Used : 87.3 GB
95
+ Free : 189.2 GB
96
+ Usage : 30.0%
97
+ System: Arch Linux (pacman)
98
+
99
+ UPDATES (3 available, ~150 MB to download)
100
+ 🔴 CRITICAL openssl 3.0.1 -> 3.0.2 Security-related update (2.5 MB / 9 MB)
101
+ 🟠 HIGH linux 6.0.1 -> 6.0.2 Kernel package — needs reboot (38 MB / 700 MB)
102
+ 🟡 MEDIUM figlet 2.2.5 -> 2.2.6 No security metadata detected (0.5 MB / 1 MB)
103
+ ```
104
+
105
+ ## How priorities work
106
+
107
+ PkgWise collects only **observable facts** about a package — security metadata, whether it
108
+ is a kernel, a core system component or a system library — and maps them through a small,
109
+ documented scoring table in `pkgwise/core/priority.py`. Labels always come with their
110
+ reasons, so nothing is ever marked important without an explanation.
111
+
112
+ ## Safety model
113
+
114
+ | Guarantee | Where enforced |
115
+ | --- | --- |
116
+ | Nothing runs automatically | `cli.py`, TUI confirm screens |
117
+ | Exact command shown before running | backends' `install_command()` |
118
+ | Empty selection can't upgrade everything | backends return `[]` for empty selections |
119
+ | Disk-space check before install | `analyzer.prepare_update()` |
120
+ | Privilege check before install | `analyzer.prepare_update()` |
121
+ | No packages removed or cleaned | analyzer never invokes remove/clean |
122
+
123
+ ## Development
124
+
125
+ ```bash
126
+ python3 -m venv .venv && source .venv/bin/activate
127
+ pip install -e ".[all,test]"
128
+ pytest
129
+ ```
130
+
131
+ See [CONTRIBUTING](docs/CONTRIBUTING.md) and [DEVELOPMENT](docs/DEVELOPMENT.md).
132
+
133
+ ## License
134
+
135
+ Released under the [MIT License](LICENSE).
@@ -0,0 +1,100 @@
1
+ <h1 align="center">PkgWise</h1>
2
+
3
+ <p align="center">
4
+ <b>Know what you're updating before you update.</b><br/>
5
+ A conservative Linux package-update analyzer for Arch (pacman) and Debian-family (apt) systems.
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="https://github.com/Senpi-Station/PkgWise/actions"><img src="https://github.com/Senpi-Station/PkgWise/actions/workflows/ci.yaml/badge.svg" alt="CI"></a>
10
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
11
+ <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.9%2B-blue" alt="Python 3.9+"></a>
12
+ </p>
13
+
14
+ PkgWise checks for available updates **without installing anything**, shows you what
15
+ is waiting (name, versions, sizes, security and reboot hints), scores each update with
16
+ a **transparent, conservative priority**, and only ever runs a real update after you
17
+ explicitly confirm the exact command.
18
+
19
+ ## Features
20
+
21
+ - **Dual package managers** — `pacman` (Arch) and `apt-get`/`apt-cache` (Debian, Kali, Ubuntu).
22
+ - **Sizes you can plan with** — download and installed sizes are read from the package
23
+ manager's own metadata, never guessed.
24
+ - **Transparent priorities** — every CRITICAL/HIGH/MEDIUM/LOW label comes with the
25
+ human-readable reasons behind it. Unknown facts are shown as “Unknown”, never invented.
26
+ - **Security-first updates** — nothing runs automatically, nothing is removed or cleaned,
27
+ empty selections can never trigger a full-system upgrade, and disk-space + privilege
28
+ checks gate every real update.
29
+ - **Two interfaces** — a fast terminal CLI and a Textual TUI with selection, detail and
30
+ confirmation screens.
31
+ - **Mock mode** — preview the whole tool on any machine without touching a real package
32
+ manager.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install pkgwise # core (storage + CLI)
38
+ pip install "pkgwise[all]" # + TUI (textual) and enhanced storage stats (psutil)
39
+ ```
40
+
41
+ Requires Python 3.9+. Works on Arch-based and Debian-based Linux systems.
42
+
43
+ ## Quick start
44
+
45
+ ```console
46
+ $ pkgwise check # storage summary + available updates, prioritized
47
+ $ pkgwise updates # just the pending updates
48
+ $ pkgwise storage # disk information only (no package manager needed)
49
+ $ pkgwise --refresh # refresh package indexes first (network)
50
+ $ pkgwise # launch the interactive TUI
51
+ $ pkgwise --version # version and system info
52
+ ```
53
+
54
+ ## Example output
55
+
56
+ ```
57
+ STORAGE
58
+ Total : 291.4 GB
59
+ Used : 87.3 GB
60
+ Free : 189.2 GB
61
+ Usage : 30.0%
62
+ System: Arch Linux (pacman)
63
+
64
+ UPDATES (3 available, ~150 MB to download)
65
+ 🔴 CRITICAL openssl 3.0.1 -> 3.0.2 Security-related update (2.5 MB / 9 MB)
66
+ 🟠 HIGH linux 6.0.1 -> 6.0.2 Kernel package — needs reboot (38 MB / 700 MB)
67
+ 🟡 MEDIUM figlet 2.2.5 -> 2.2.6 No security metadata detected (0.5 MB / 1 MB)
68
+ ```
69
+
70
+ ## How priorities work
71
+
72
+ PkgWise collects only **observable facts** about a package — security metadata, whether it
73
+ is a kernel, a core system component or a system library — and maps them through a small,
74
+ documented scoring table in `pkgwise/core/priority.py`. Labels always come with their
75
+ reasons, so nothing is ever marked important without an explanation.
76
+
77
+ ## Safety model
78
+
79
+ | Guarantee | Where enforced |
80
+ | --- | --- |
81
+ | Nothing runs automatically | `cli.py`, TUI confirm screens |
82
+ | Exact command shown before running | backends' `install_command()` |
83
+ | Empty selection can't upgrade everything | backends return `[]` for empty selections |
84
+ | Disk-space check before install | `analyzer.prepare_update()` |
85
+ | Privilege check before install | `analyzer.prepare_update()` |
86
+ | No packages removed or cleaned | analyzer never invokes remove/clean |
87
+
88
+ ## Development
89
+
90
+ ```bash
91
+ python3 -m venv .venv && source .venv/bin/activate
92
+ pip install -e ".[all,test]"
93
+ pytest
94
+ ```
95
+
96
+ See [CONTRIBUTING](docs/CONTRIBUTING.md) and [DEVELOPMENT](docs/DEVELOPMENT.md).
97
+
98
+ ## License
99
+
100
+ Released under the [MIT License](LICENSE).
@@ -0,0 +1,12 @@
1
+ """PkgWise — Smart Linux Update Analyzer.
2
+
3
+ Know what you're updating before you update.
4
+
5
+ PkgWise is an analysis-first package update assistant. It uses the native
6
+ package manager (currently ``pacman`` and ``apt``) as its backend and provides
7
+ a better interface for understanding and selecting package updates.
8
+ """
9
+
10
+ __version__ = "0.1.0"
11
+
12
+ __all__ = ["__version__"]
@@ -0,0 +1,6 @@
1
+ """Module entry point: ``python -m pkgwise``."""
2
+
3
+ from pkgwise.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1 @@
1
+ """Package-manager backends: pacman (Arch), apt (Debian) and the shared interface."""
@@ -0,0 +1,223 @@
1
+ """Arch Linux backend — wraps the ``pacman`` package manager.
2
+
3
+ Parser details
4
+ --------------
5
+ ``pacman -Qu`` lists outdated packages as ``name current => new``.
6
+ ``pacman -Si <name>...`` reads the local sync databases (no network) and
7
+ reports per-package ``Download Size`` and ``Installed Size`` for the *new*
8
+ version, so sizes are realistic for planning an update.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from typing import Dict, List, Optional
15
+
16
+ from pkgwise.backends.base import InstallError, PackageManager
17
+ from pkgwise.core.models import PackageUpdate
18
+ from pkgwise.utils.commands import CommandError, run_command
19
+
20
+ _QU_LINE = re.compile(r"^(?P<name>[^\s]+)\s+(?P<current>[^\s]+)\s*=>\s*(?P<new>[^\s]+)$")
21
+ _SI_FIELD = re.compile(r"^(?P<key>[^:]+):(?P<value>.*)$")
22
+
23
+ _UPDATE_COMMAND = "-Su"
24
+
25
+
26
+ class ArchBackend(PackageManager):
27
+ """pacman-backed implementation for Arch Linux and derivatives."""
28
+
29
+ executable = "pacman"
30
+ name = "pacman"
31
+
32
+ @property
33
+ def supported(self) -> bool:
34
+ import shutil
35
+
36
+ return shutil.which("pacman") is not None
37
+
38
+ # -- parsing -----------------------------------------------------------
39
+
40
+ def check_updates(self) -> List[PackageUpdate]:
41
+ """Read available updates from the local sync databases (no refresh)."""
42
+ lines = self._run_pacman(["-Qu"]).stdout.splitlines()
43
+ parsed = [_parse_qu_line(line) for line in lines]
44
+ updates: List[PackageUpdate] = []
45
+ for row in parsed:
46
+ if row is None:
47
+ continue
48
+ name, current, new = row
49
+ updates.append(
50
+ PackageUpdate(
51
+ name=name,
52
+ current_version=current,
53
+ new_version=new,
54
+ package_manager=self.name,
55
+ )
56
+ )
57
+ if not updates:
58
+ return updates
59
+ self._attach_sizes(updates)
60
+ return updates
61
+
62
+ def list_updates(self) -> List[PackageUpdate]:
63
+ """Refresh sync databases, then return available updates."""
64
+ self._sync_databases()
65
+ return self.check_updates()
66
+
67
+ def install_updates(self, names: List[str]) -> None:
68
+ """Build an update transaction for *names* and run it.
69
+
70
+ Authorization (root/sudo) is handled by the caller before this runs;
71
+ the produced command is deterministic and uses ``--noconfirm`` because
72
+ PkgWise already collected an explicit confirmation.
73
+ """
74
+ if not names:
75
+ return
76
+ _run_real_install("pacman", self.install_command(names), names)
77
+
78
+ def install_command(self, names: List[str]) -> List[str]:
79
+ """Return the ``pacman`` invocation used to update *names*.
80
+
81
+ An empty selection yields an empty command: with no explicit targets
82
+ ``pacman -Su`` would upgrade the entire system, which PkgWise must
83
+ never trigger by accident.
84
+ """
85
+ selected = sorted(set(names))
86
+ if not selected:
87
+ return []
88
+ return ["pacman", "--noconfirm", _UPDATE_COMMAND, *selected]
89
+
90
+ # -- internal helpers --------------------------------------------------
91
+
92
+ def _sync_databases(self) -> None:
93
+ """Refresh the sync databases (``pacman -Sy``); read-only regarding packages."""
94
+ run_command(["pacman", "-Sy", "--noconfirm"], timeout=300, check=False)
95
+
96
+ def _run_pacman(self, args: List[str]):
97
+ return run_command(["pacman", *args], timeout=180)
98
+
99
+ def _attach_sizes(self, updates: List[PackageUpdate]) -> None:
100
+ """Populate download/installed sizes using ``pacman -Si`` (offline)."""
101
+ if not updates:
102
+ return
103
+ names = [u.name for u in updates]
104
+ try:
105
+ output = self._run_pacman(["-Si", *names]).stdout
106
+ except CommandError:
107
+ return
108
+ sizes = _parse_si_blocks(output)
109
+ for index, update in enumerate(updates):
110
+ info = sizes.get(update.name)
111
+ if info is None:
112
+ continue
113
+ updated = replaceable(update, info)
114
+ if updated is not update:
115
+ updates[index] = updated
116
+
117
+
118
+ def replaceable(update: PackageUpdate, info: Dict[str, Optional[int]]) -> PackageUpdate:
119
+ """Return an updated copy only when *info* provides reliable values."""
120
+ from dataclasses import replace
121
+
122
+ overrides = {}
123
+ if info.get("download") is not None:
124
+ overrides["download_size"] = info["download"]
125
+ if info.get("installed") is not None:
126
+ overrides["installed_size"] = info["installed"]
127
+ if not overrides:
128
+ return update
129
+ return replace(update, **overrides)
130
+
131
+
132
+ def _parse_qu_line(line: str) -> Optional[tuple]:
133
+ """Parse one ``pacman -Qu`` output line into (name, current, new)."""
134
+ stripped = line.strip()
135
+ if not stripped:
136
+ return None
137
+ match = _QU_LINE.match(stripped)
138
+ if not match:
139
+ return None
140
+ return (match.group("name"), match.group("current"), match.group("new"))
141
+
142
+
143
+ def _parse_si_blocks(output: str) -> Dict[str, Dict[str, Optional[int]]]:
144
+ """Parse consolidated ``pacman -Si`` output into ``{name: sizes}``.
145
+
146
+ Sizes are converted from strings such as ``8.25 MiB`` to integer bytes.
147
+ Returns an empty dict when nothing parsable is found.
148
+ """
149
+ result: Dict[str, Dict[str, Optional[int]]] = {}
150
+ current: Dict[str, str] = {}
151
+ for raw in output.splitlines():
152
+ line = raw.strip()
153
+ if not line and current:
154
+ _commit_si_block(result, current)
155
+ current = {}
156
+ continue
157
+ match = _SI_FIELD.match(line)
158
+ if not match:
159
+ continue
160
+ key = match.group("key").strip().lower()
161
+ value = match.group("value").strip()
162
+ if key in {"name", "version", "download size", "installed size"}:
163
+ current[key] = value
164
+ if current:
165
+ _commit_si_block(result, current)
166
+ return result
167
+
168
+
169
+ def _commit_si_block(result: Dict[str, Dict[str, Optional[int]]], block: Dict[str, str]) -> None:
170
+ name = block.get("name")
171
+ if not name:
172
+ return
173
+ entry = {
174
+ "download": _parse_size(block.get("download size")),
175
+ "installed": _parse_size(block.get("installed size")),
176
+ }
177
+ result[name] = entry
178
+
179
+
180
+ def _parse_size(raw: Optional[str]) -> Optional[int]:
181
+ """Parse pacman sizes like ``408.88 KiB`` / ``4.02 MiB`` into bytes."""
182
+ if not raw:
183
+ return None
184
+ match = re.match(r"^([0-9.]+)\s*([A-Za-z]+)?$", raw.strip())
185
+ if not match:
186
+ return None
187
+ try:
188
+ value = float(match.group(1))
189
+ except ValueError:
190
+ return None
191
+ unit = (match.group(2) or "B").lower()
192
+ multiplier = {
193
+ "b": 1,
194
+ "kib": 1024,
195
+ "mib": 1024**2,
196
+ "gib": 1024**3,
197
+ "tib": 1024**4,
198
+ }.get(unit, 1)
199
+ return int(value * multiplier)
200
+
201
+
202
+ def _run_real_install(manager: str, command: List[str], names: List[str]) -> None:
203
+ """Execute a real install/update command, converting failures to InstallError.
204
+
205
+ Never constructs a shell string — always an argument list.
206
+ """
207
+ from pkgwise.utils import commands as cmd_utils
208
+
209
+ path = cmd_utils.which(manager)
210
+ if path is None:
211
+ raise InstallError(
212
+ f"The package manager '{manager}' is not installed.",
213
+ detail="Install it before retrying an update.",
214
+ )
215
+ try:
216
+ cmd_utils.run_command(command, timeout=_INSTALL_TIMEOUT, check=False)
217
+ except cmd_utils.CommandError as exc:
218
+ raise InstallError(
219
+ exc.message, returncode=exc.returncode, detail=exc.detail
220
+ ) from exc
221
+
222
+
223
+ _INSTALL_TIMEOUT = 86_400 # 24h: installs can legitimately take a long time
@@ -0,0 +1,107 @@
1
+ """Abstract package-manager interface.
2
+
3
+ The TUI and analyzer talk only to this interface. Concrete backends
4
+ (:mod:`pkgwise.backends.arch` and :mod:`pkgwise.backends.debian`)
5
+ implement the same contract, so adding Fedora/dnf later means adding one new
6
+ backend class — no changes to the UI or analyzer.
7
+
8
+ Parsers return raw parsed values (``None`` when unknown); assigning priority
9
+ is exclusively the job of :mod:`pkgwise.core.priority`.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from abc import ABC, abstractmethod
15
+ from typing import List, Optional
16
+
17
+ from pkgwise.core.models import PackageUpdate
18
+
19
+
20
+ class UpdateCheckError(Exception):
21
+ """Base error for any backend query failure. Raised with a human message."""
22
+
23
+ def __init__(self, message: str, detail: Optional[str] = None):
24
+ self.message = message
25
+ self.detail = detail
26
+ super().__init__(message)
27
+
28
+
29
+ class BackendUnavailableError(UpdateCheckError):
30
+ """The package-manager binary is not installed on this system."""
31
+
32
+
33
+ class UnsupportedDistroError(UpdateCheckError):
34
+ """No backend maps to the detected distribution/package manager."""
35
+
36
+
37
+ class PackageManager(ABC):
38
+ """Common contract every backend implements."""
39
+
40
+ #: Binary invoked to perform update operations.
41
+ executable: str = ""
42
+
43
+ #: Human name for the backend, e.g. "pacman".
44
+ name: str = ""
45
+
46
+ @property
47
+ @abstractmethod
48
+ def supported(self) -> bool:
49
+ """True when this backend's package manager is installed."""
50
+
51
+ @abstractmethod
52
+ def check_updates(self) -> List[PackageUpdate]:
53
+ """Query and return available updates (read-only, no refresh)."""
54
+
55
+ @abstractmethod
56
+ def list_updates(self) -> List[PackageUpdate]:
57
+ """Update the package index, then return available updates."""
58
+
59
+ @abstractmethod
60
+ def install_updates(self, names: List[str]) -> None:
61
+ """Perform a real update for the given package names.
62
+
63
+ May raise :class:`InstallError` on failure. Intended to be run from
64
+ middleware that has already checked permissions and disk space.
65
+ """
66
+
67
+ @abstractmethod
68
+ def install_command(self, names: List[str]) -> List[str]:
69
+ """Return the argument list that would update *names* (no side effects).
70
+
71
+ Exists so callers can display/confirm the exact command first.
72
+ """
73
+
74
+
75
+ class InstallError(Exception):
76
+ """Raised when an update operation fails."""
77
+
78
+ def __init__(self, message: str, returncode: Optional[int] = None, detail: Optional[str] = None):
79
+ self.message = message
80
+ self.returncode = returncode
81
+ self.detail = detail
82
+ super().__init__(message)
83
+
84
+
85
+ def _as_int(raw: Optional[str]) -> Optional[int]:
86
+ """Parse a size/count string (often with commas or units) to int."""
87
+ if not raw:
88
+ return None
89
+ cleaned = raw.replace(",", "").replace(" ", "").strip()
90
+ if not cleaned:
91
+ return None
92
+ try:
93
+ return int(cleaned)
94
+ except (ValueError, TypeError):
95
+ return None
96
+
97
+
98
+ def _yes_no(raw: Optional[str]) -> Optional[bool]:
99
+ """Parse a y/n style value into a bool, or ``None`` if unparseable."""
100
+ if raw is None:
101
+ return None
102
+ lowered = raw.strip().lower()
103
+ if lowered in {"y", "yes", "true", "1"}:
104
+ return True
105
+ if lowered in {"n", "no", "false", "0"}:
106
+ return False
107
+ return None