killpy 0.19.2__tar.gz → 0.21.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.
- {killpy-0.19.2 → killpy-0.21.0}/PKG-INFO +114 -2
- {killpy-0.19.2 → killpy-0.21.0}/README.md +113 -1
- {killpy-0.19.2 → killpy-0.21.0}/killpy/__main__.py +9 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/cli.py +52 -5
- {killpy-0.19.2 → killpy-0.21.0}/killpy/commands/delete.py +8 -3
- killpy-0.21.0/killpy/commands/doctor.py +291 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/commands/stats.py +38 -1
- {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/conda.py +4 -3
- {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/pipx.py +32 -26
- killpy-0.21.0/killpy/intelligence/__init__.py +56 -0
- killpy-0.21.0/killpy/intelligence/git_analyzer.py +119 -0
- killpy-0.21.0/killpy/intelligence/scoring.py +227 -0
- killpy-0.21.0/killpy/intelligence/suggestions.py +120 -0
- killpy-0.21.0/killpy/intelligence/tracker.py +123 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/models.py +82 -2
- {killpy-0.19.2 → killpy-0.21.0}/killpy.egg-info/PKG-INFO +114 -2
- {killpy-0.19.2 → killpy-0.21.0}/killpy.egg-info/SOURCES.txt +7 -1
- {killpy-0.19.2 → killpy-0.21.0}/pyproject.toml +11 -3
- {killpy-0.19.2 → killpy-0.21.0}/killpy/__init__.py +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/cleaner.py +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/cleaners/__init__.py +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/commands/__init__.py +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/commands/clean.py +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/commands/list.py +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/__init__.py +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/artifacts.py +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/base.py +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/cache.py +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/hatch.py +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/pipenv.py +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/poetry.py +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/pyenv.py +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/tox.py +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/uv.py +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/detectors/venv.py +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/files/__init__.py +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy/scanner.py +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy.egg-info/dependency_links.txt +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy.egg-info/entry_points.txt +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy.egg-info/requires.txt +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/killpy.egg-info/top_level.txt +0 -0
- {killpy-0.19.2 → killpy-0.21.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: killpy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.21.0
|
|
4
4
|
Summary: List all .venv directories and Conda environments 🐍 on your system and check how much space they are using. You can then choose which ones to delete in order to free up space 🧹.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Tlaloc-Es/killpy
|
|
6
6
|
Project-URL: Documentation, https://tlaloc-es.github.io/killpy/
|
|
@@ -224,6 +224,24 @@ killpy list --json # output as a JSON array
|
|
|
224
224
|
killpy list --json-stream # stream as NDJSON — one line per env
|
|
225
225
|
```
|
|
226
226
|
|
|
227
|
+
`--json` example output:
|
|
228
|
+
|
|
229
|
+
```json
|
|
230
|
+
[
|
|
231
|
+
{
|
|
232
|
+
"path": "projects/my-app/.venv",
|
|
233
|
+
"absolute_path": "/home/user/projects/my-app/.venv",
|
|
234
|
+
"name": "my-app/.venv",
|
|
235
|
+
"type": "venv",
|
|
236
|
+
"last_accessed": "2025-11-02T14:23:01+00:00",
|
|
237
|
+
"size_bytes": 54393984,
|
|
238
|
+
"size_human": "51.88 MB",
|
|
239
|
+
"managed_by": null,
|
|
240
|
+
"is_system_critical": false
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
```
|
|
244
|
+
|
|
227
245
|
`--json-stream` is ideal for piping into `jq` or processing in scripts before the full scan completes:
|
|
228
246
|
|
|
229
247
|
```bash
|
|
@@ -252,6 +270,7 @@ ______________________________________________________________________
|
|
|
252
270
|
killpy stats
|
|
253
271
|
killpy stats --path ~/projects
|
|
254
272
|
killpy stats --json
|
|
273
|
+
killpy stats --history # cumulative scan history
|
|
255
274
|
```
|
|
256
275
|
|
|
257
276
|
Example output:
|
|
@@ -283,6 +302,98 @@ Removes `__pycache__` directories recursively under the target path.
|
|
|
283
302
|
|
|
284
303
|
______________________________________________________________________
|
|
285
304
|
|
|
305
|
+
### `killpy doctor` — smart health report
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
Usage: killpy doctor [OPTIONS]
|
|
309
|
+
|
|
310
|
+
Options:
|
|
311
|
+
--path DIRECTORY Root directory to scan [default: cwd]
|
|
312
|
+
--all Show all environments grouped by category
|
|
313
|
+
(HIGH / MEDIUM / LOW). Default shows only the top 5.
|
|
314
|
+
--json Output as JSON.
|
|
315
|
+
--help Show this message and exit.
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
`doctor` analyses every detected virtual environment in two phases:
|
|
319
|
+
|
|
320
|
+
**Phase 1 — Scoring (for sorting only)**
|
|
321
|
+
|
|
322
|
+
A numeric score between 0 and 1 is computed from four weighted signals:
|
|
323
|
+
|
|
324
|
+
| Signal | Description |
|
|
325
|
+
|--------|-------------|
|
|
326
|
+
| **Size** | Larger environments score higher (sigmoid-normalised around 500 MB). |
|
|
327
|
+
| **Age** | Days since last access, linear up to 365 days. |
|
|
328
|
+
| **Orphan status** | No `pyproject.toml`, `requirements.txt`, or other project marker found nearby. |
|
|
329
|
+
| **Git inactivity** | The associated git repository has no recent commits. |
|
|
330
|
+
|
|
331
|
+
The score determines *ordering* within each category (highest score listed first). It does **not** determine the category itself.
|
|
332
|
+
|
|
333
|
+
**Phase 2 — Rule-based classification**
|
|
334
|
+
|
|
335
|
+
Category is assigned deterministically by applying the following rules in order:
|
|
336
|
+
|
|
337
|
+
| Priority | Rule | Category |
|
|
338
|
+
|----------|------|----------|
|
|
339
|
+
| 1 | Orphan (`is_orphan=True`) **and** `age ≥ 180 days` | `HIGH` |
|
|
340
|
+
| 2 | No project files **and** `age ≥ 365 days` | `HIGH` |
|
|
341
|
+
| 3 | Active git repository **or** `age < 120 days` | `LOW` |
|
|
342
|
+
| 4 | `age ≥ 120 days` | `MEDIUM` |
|
|
343
|
+
| 5 | Fallback | `LOW` |
|
|
344
|
+
|
|
345
|
+
Age and orphan status are the dominant signals. Size does not affect the category.
|
|
346
|
+
|
|
347
|
+
| Category | Recommended action |
|
|
348
|
+
|----------|--------------------|
|
|
349
|
+
| `HIGH` | Delete — unused and orphaned |
|
|
350
|
+
| `MEDIUM` | Review — possibly unused |
|
|
351
|
+
| `LOW` | Keep — actively used / Keep |
|
|
352
|
+
|
|
353
|
+
Examples:
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
killpy doctor # top 5 offenders in current directory
|
|
357
|
+
killpy doctor --path ~ # scan home folder
|
|
358
|
+
killpy doctor --all # show all environments by category
|
|
359
|
+
killpy doctor --json # machine-readable output
|
|
360
|
+
killpy doctor --path ~/projects --all # full report for a specific tree
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Example output (default):
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
──────────── Environment Health Report ────────────
|
|
367
|
+
Scanned: /home/user/projects
|
|
368
|
+
Environments found: 18 | Total size: 6.2 GB | Estimated wasted: 3.8 GB
|
|
369
|
+
HIGH (safe to delete): 5 MEDIUM (review): 7 LOW (keep): 6
|
|
370
|
+
|
|
371
|
+
Top 5 Offenders
|
|
372
|
+
┌──────────────────────────┬────────┬───────────┬───────┬──────────┐
|
|
373
|
+
│ Path │ Size │ Age (days)│ Score │ Category │
|
|
374
|
+
├──────────────────────────┼────────┼───────────┼───────┼──────────┤
|
|
375
|
+
│ ~/old-project/.venv │ 850 MB │ 312 │ 0.94 │ HIGH │
|
|
376
|
+
│ ~/tutorial2023/.venv │ 420 MB │ 198 │ 0.87 │ HIGH │
|
|
377
|
+
└──────────────────────────┴────────┴───────────┴───────┴──────────┘
|
|
378
|
+
|
|
379
|
+
Recommendation: Run `killpy delete --older-than 180` to free up to 3.8 GB.
|
|
380
|
+
(12 MEDIUM/LOW environment(s) hidden — run with --all to see them)
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Use `--all` to see all three category tables at once:
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
killpy doctor --all
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
The JSON output is useful for scripting or auditing:
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
killpy doctor --json | jq '.suggestions[] | select(.category=="HIGH") | .env_path'
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
______________________________________________________________________
|
|
396
|
+
|
|
286
397
|
## killpy vs alternatives
|
|
287
398
|
|
|
288
399
|
| Tool | venv | conda | poetry | pipx | pyenv | caches | artifacts | TUI | search | multi-select |
|
|
@@ -410,7 +521,7 @@ Keep your repo free of `__pycache__` on every commit:
|
|
|
410
521
|
|
|
411
522
|
```yaml
|
|
412
523
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
413
|
-
rev: 0.
|
|
524
|
+
rev: 0.21.0
|
|
414
525
|
hooks:
|
|
415
526
|
- id: killpy
|
|
416
527
|
pass_filenames: false
|
|
@@ -437,6 +548,7 @@ ______________________________________________________________________
|
|
|
437
548
|
- [ ] Windows support improvements (pyenv-win, conda on Windows PATH)
|
|
438
549
|
- [ ] TUI: filter panel, live progress bar, confirmation dialog with total bytes before delete
|
|
439
550
|
- [ ] Detect unused dependencies inside `pyproject.toml` / `requirements.txt`
|
|
551
|
+
- [ ] Add environment reporting (Python version, installed packages, and versions) to better differentiate between virtual environments with different dependencies
|
|
440
552
|
|
|
441
553
|
______________________________________________________________________
|
|
442
554
|
|
|
@@ -204,6 +204,24 @@ killpy list --json # output as a JSON array
|
|
|
204
204
|
killpy list --json-stream # stream as NDJSON — one line per env
|
|
205
205
|
```
|
|
206
206
|
|
|
207
|
+
`--json` example output:
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
[
|
|
211
|
+
{
|
|
212
|
+
"path": "projects/my-app/.venv",
|
|
213
|
+
"absolute_path": "/home/user/projects/my-app/.venv",
|
|
214
|
+
"name": "my-app/.venv",
|
|
215
|
+
"type": "venv",
|
|
216
|
+
"last_accessed": "2025-11-02T14:23:01+00:00",
|
|
217
|
+
"size_bytes": 54393984,
|
|
218
|
+
"size_human": "51.88 MB",
|
|
219
|
+
"managed_by": null,
|
|
220
|
+
"is_system_critical": false
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
```
|
|
224
|
+
|
|
207
225
|
`--json-stream` is ideal for piping into `jq` or processing in scripts before the full scan completes:
|
|
208
226
|
|
|
209
227
|
```bash
|
|
@@ -232,6 +250,7 @@ ______________________________________________________________________
|
|
|
232
250
|
killpy stats
|
|
233
251
|
killpy stats --path ~/projects
|
|
234
252
|
killpy stats --json
|
|
253
|
+
killpy stats --history # cumulative scan history
|
|
235
254
|
```
|
|
236
255
|
|
|
237
256
|
Example output:
|
|
@@ -263,6 +282,98 @@ Removes `__pycache__` directories recursively under the target path.
|
|
|
263
282
|
|
|
264
283
|
______________________________________________________________________
|
|
265
284
|
|
|
285
|
+
### `killpy doctor` — smart health report
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
Usage: killpy doctor [OPTIONS]
|
|
289
|
+
|
|
290
|
+
Options:
|
|
291
|
+
--path DIRECTORY Root directory to scan [default: cwd]
|
|
292
|
+
--all Show all environments grouped by category
|
|
293
|
+
(HIGH / MEDIUM / LOW). Default shows only the top 5.
|
|
294
|
+
--json Output as JSON.
|
|
295
|
+
--help Show this message and exit.
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
`doctor` analyses every detected virtual environment in two phases:
|
|
299
|
+
|
|
300
|
+
**Phase 1 — Scoring (for sorting only)**
|
|
301
|
+
|
|
302
|
+
A numeric score between 0 and 1 is computed from four weighted signals:
|
|
303
|
+
|
|
304
|
+
| Signal | Description |
|
|
305
|
+
|--------|-------------|
|
|
306
|
+
| **Size** | Larger environments score higher (sigmoid-normalised around 500 MB). |
|
|
307
|
+
| **Age** | Days since last access, linear up to 365 days. |
|
|
308
|
+
| **Orphan status** | No `pyproject.toml`, `requirements.txt`, or other project marker found nearby. |
|
|
309
|
+
| **Git inactivity** | The associated git repository has no recent commits. |
|
|
310
|
+
|
|
311
|
+
The score determines *ordering* within each category (highest score listed first). It does **not** determine the category itself.
|
|
312
|
+
|
|
313
|
+
**Phase 2 — Rule-based classification**
|
|
314
|
+
|
|
315
|
+
Category is assigned deterministically by applying the following rules in order:
|
|
316
|
+
|
|
317
|
+
| Priority | Rule | Category |
|
|
318
|
+
|----------|------|----------|
|
|
319
|
+
| 1 | Orphan (`is_orphan=True`) **and** `age ≥ 180 days` | `HIGH` |
|
|
320
|
+
| 2 | No project files **and** `age ≥ 365 days` | `HIGH` |
|
|
321
|
+
| 3 | Active git repository **or** `age < 120 days` | `LOW` |
|
|
322
|
+
| 4 | `age ≥ 120 days` | `MEDIUM` |
|
|
323
|
+
| 5 | Fallback | `LOW` |
|
|
324
|
+
|
|
325
|
+
Age and orphan status are the dominant signals. Size does not affect the category.
|
|
326
|
+
|
|
327
|
+
| Category | Recommended action |
|
|
328
|
+
|----------|--------------------|
|
|
329
|
+
| `HIGH` | Delete — unused and orphaned |
|
|
330
|
+
| `MEDIUM` | Review — possibly unused |
|
|
331
|
+
| `LOW` | Keep — actively used / Keep |
|
|
332
|
+
|
|
333
|
+
Examples:
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
killpy doctor # top 5 offenders in current directory
|
|
337
|
+
killpy doctor --path ~ # scan home folder
|
|
338
|
+
killpy doctor --all # show all environments by category
|
|
339
|
+
killpy doctor --json # machine-readable output
|
|
340
|
+
killpy doctor --path ~/projects --all # full report for a specific tree
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Example output (default):
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
──────────── Environment Health Report ────────────
|
|
347
|
+
Scanned: /home/user/projects
|
|
348
|
+
Environments found: 18 | Total size: 6.2 GB | Estimated wasted: 3.8 GB
|
|
349
|
+
HIGH (safe to delete): 5 MEDIUM (review): 7 LOW (keep): 6
|
|
350
|
+
|
|
351
|
+
Top 5 Offenders
|
|
352
|
+
┌──────────────────────────┬────────┬───────────┬───────┬──────────┐
|
|
353
|
+
│ Path │ Size │ Age (days)│ Score │ Category │
|
|
354
|
+
├──────────────────────────┼────────┼───────────┼───────┼──────────┤
|
|
355
|
+
│ ~/old-project/.venv │ 850 MB │ 312 │ 0.94 │ HIGH │
|
|
356
|
+
│ ~/tutorial2023/.venv │ 420 MB │ 198 │ 0.87 │ HIGH │
|
|
357
|
+
└──────────────────────────┴────────┴───────────┴───────┴──────────┘
|
|
358
|
+
|
|
359
|
+
Recommendation: Run `killpy delete --older-than 180` to free up to 3.8 GB.
|
|
360
|
+
(12 MEDIUM/LOW environment(s) hidden — run with --all to see them)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Use `--all` to see all three category tables at once:
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
killpy doctor --all
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
The JSON output is useful for scripting or auditing:
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
killpy doctor --json | jq '.suggestions[] | select(.category=="HIGH") | .env_path'
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
______________________________________________________________________
|
|
376
|
+
|
|
266
377
|
## killpy vs alternatives
|
|
267
378
|
|
|
268
379
|
| Tool | venv | conda | poetry | pipx | pyenv | caches | artifacts | TUI | search | multi-select |
|
|
@@ -390,7 +501,7 @@ Keep your repo free of `__pycache__` on every commit:
|
|
|
390
501
|
|
|
391
502
|
```yaml
|
|
392
503
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
393
|
-
rev: 0.
|
|
504
|
+
rev: 0.21.0
|
|
394
505
|
hooks:
|
|
395
506
|
- id: killpy
|
|
396
507
|
pass_filenames: false
|
|
@@ -417,6 +528,7 @@ ______________________________________________________________________
|
|
|
417
528
|
- [ ] Windows support improvements (pyenv-win, conda on Windows PATH)
|
|
418
529
|
- [ ] TUI: filter panel, live progress bar, confirmation dialog with total bytes before delete
|
|
419
530
|
- [ ] Detect unused dependencies inside `pyproject.toml` / `requirements.txt`
|
|
531
|
+
- [ ] Add environment reporting (Python version, installed packages, and versions) to better differentiate between virtual environments with different dependencies
|
|
420
532
|
|
|
421
533
|
______________________________________________________________________
|
|
422
534
|
|
|
@@ -9,9 +9,11 @@ from killpy.cleaner import Cleaner
|
|
|
9
9
|
from killpy.cli import TableApp
|
|
10
10
|
from killpy.commands.clean import clean
|
|
11
11
|
from killpy.commands.delete import delete_cmd
|
|
12
|
+
from killpy.commands.doctor import doctor_cmd
|
|
12
13
|
from killpy.commands.list import list_cmd
|
|
13
14
|
from killpy.commands.stats import stats_cmd
|
|
14
15
|
from killpy.files import format_size
|
|
16
|
+
from killpy.intelligence.tracker import UsageTracker
|
|
15
17
|
from killpy.scanner import Scanner
|
|
16
18
|
|
|
17
19
|
|
|
@@ -63,6 +65,12 @@ def _run_delete_all(path: Path, excluded: set[str], yes: bool) -> None:
|
|
|
63
65
|
f"freed [bold]{format_size(freed)}[/bold]."
|
|
64
66
|
)
|
|
65
67
|
|
|
68
|
+
# Best-effort: record deletion in history.
|
|
69
|
+
try:
|
|
70
|
+
UsageTracker().record_deletion(freed)
|
|
71
|
+
except Exception: # noqa: BLE001
|
|
72
|
+
pass
|
|
73
|
+
|
|
66
74
|
|
|
67
75
|
@click.group(invoke_without_command=True)
|
|
68
76
|
@click.option(
|
|
@@ -109,6 +117,7 @@ cli.add_command(clean)
|
|
|
109
117
|
cli.add_command(list_cmd, name="list")
|
|
110
118
|
cli.add_command(delete_cmd, name="delete")
|
|
111
119
|
cli.add_command(stats_cmd, name="stats")
|
|
120
|
+
cli.add_command(doctor_cmd, name="doctor")
|
|
112
121
|
|
|
113
122
|
|
|
114
123
|
if __name__ == "__main__":
|
|
@@ -8,6 +8,7 @@ from importlib.metadata import PackageNotFoundError, version
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any, TypedDict
|
|
10
10
|
|
|
11
|
+
from rich.text import Text
|
|
11
12
|
from textual.app import App, ComposeResult
|
|
12
13
|
from textual.binding import Binding
|
|
13
14
|
from textual.widgets import (
|
|
@@ -24,9 +25,23 @@ from textual.widgets import (
|
|
|
24
25
|
from killpy.cleaner import Cleaner, CleanerError
|
|
25
26
|
from killpy.cleaners import remove_pycache
|
|
26
27
|
from killpy.files import format_size
|
|
28
|
+
from killpy.intelligence import SuggestionEngine, score_all
|
|
27
29
|
from killpy.models import Environment
|
|
28
30
|
from killpy.scanner import Scanner
|
|
29
31
|
|
|
32
|
+
_HEALTH_STYLES: dict[str, tuple[str, str]] = {
|
|
33
|
+
"HIGH": ("HIGH", "bold red"),
|
|
34
|
+
"MEDIUM": ("MED", "bold yellow"),
|
|
35
|
+
"LOW": ("LOW", "bold green"),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _health_text(category: str) -> Text | str:
|
|
40
|
+
if category in _HEALTH_STYLES:
|
|
41
|
+
label, style = _HEALTH_STYLES[category]
|
|
42
|
+
return Text(label, style=style)
|
|
43
|
+
return ""
|
|
44
|
+
|
|
30
45
|
|
|
31
46
|
def is_venv_tab(func):
|
|
32
47
|
def wrapper(self, *args, **kwargs):
|
|
@@ -68,6 +83,7 @@ class VenvRow(TypedDict):
|
|
|
68
83
|
last_modified: str
|
|
69
84
|
size: int
|
|
70
85
|
size_human: str
|
|
86
|
+
health: str
|
|
71
87
|
status: str
|
|
72
88
|
environment: Environment
|
|
73
89
|
|
|
@@ -87,6 +103,7 @@ class TableApp(App):
|
|
|
87
103
|
"Last Modified",
|
|
88
104
|
"Size",
|
|
89
105
|
"Size (Human Readable)",
|
|
106
|
+
"Health",
|
|
90
107
|
"Status",
|
|
91
108
|
]
|
|
92
109
|
PIPX_HEADERS = ["Package", "Size", "Size (Human Readable)", "Status"]
|
|
@@ -95,7 +112,8 @@ class TableApp(App):
|
|
|
95
112
|
VENV_COL_LAST_MODIFIED = 2
|
|
96
113
|
VENV_COL_SIZE = 3
|
|
97
114
|
VENV_COL_SIZE_HUMAN = 4
|
|
98
|
-
|
|
115
|
+
VENV_COL_HEALTH = 5
|
|
116
|
+
VENV_COL_STATUS = 6
|
|
99
117
|
|
|
100
118
|
PIPX_COL_PACKAGE = 0
|
|
101
119
|
PIPX_COL_SIZE = 1
|
|
@@ -125,6 +143,7 @@ class TableApp(App):
|
|
|
125
143
|
self._venv_display_indices: list[int] = []
|
|
126
144
|
self._multi_select_mode: bool = False
|
|
127
145
|
self._selected_venv_indices: set[int] = set()
|
|
146
|
+
self._health_by_path: dict[str, str] = {}
|
|
128
147
|
self.cleaner = Cleaner()
|
|
129
148
|
self.scanner = Scanner(
|
|
130
149
|
types={
|
|
@@ -315,6 +334,7 @@ class TableApp(App):
|
|
|
315
334
|
"last_modified": environment.last_accessed_str,
|
|
316
335
|
"size": environment.size_bytes,
|
|
317
336
|
"size_human": environment.size_human,
|
|
337
|
+
"health": self._health_by_path.get(str(environment.path), ""),
|
|
318
338
|
"status": "",
|
|
319
339
|
"environment": environment,
|
|
320
340
|
}
|
|
@@ -332,6 +352,7 @@ class TableApp(App):
|
|
|
332
352
|
row["last_modified"],
|
|
333
353
|
row["size"],
|
|
334
354
|
row["size_human"],
|
|
355
|
+
_health_text(row["health"]),
|
|
335
356
|
row["status"],
|
|
336
357
|
)
|
|
337
358
|
|
|
@@ -373,6 +394,7 @@ class TableApp(App):
|
|
|
373
394
|
row["last_modified"],
|
|
374
395
|
row["size"],
|
|
375
396
|
row["size_human"],
|
|
397
|
+
_health_text(row["health"]),
|
|
376
398
|
self._compute_row_status(i, row),
|
|
377
399
|
)
|
|
378
400
|
|
|
@@ -453,6 +475,11 @@ class TableApp(App):
|
|
|
453
475
|
self.venv_rows.sort(
|
|
454
476
|
key=lambda row: row["size_human"].lower(), reverse=reverse
|
|
455
477
|
)
|
|
478
|
+
elif column_index == self.VENV_COL_HEALTH:
|
|
479
|
+
_order = {"HIGH": 0, "MEDIUM": 1, "LOW": 2, "": 3}
|
|
480
|
+
self.venv_rows.sort(
|
|
481
|
+
key=lambda row: _order.get(row["health"], 3), reverse=reverse
|
|
482
|
+
)
|
|
456
483
|
elif column_index == self.VENV_COL_STATUS:
|
|
457
484
|
self.venv_rows.sort(key=lambda row: row["status"].lower(), reverse=reverse)
|
|
458
485
|
else:
|
|
@@ -545,6 +572,21 @@ class TableApp(App):
|
|
|
545
572
|
status_label.update(
|
|
546
573
|
f"Found {venv_count} virtual environments and {pipx_count} pipx packages"
|
|
547
574
|
)
|
|
575
|
+
await self._compute_health_scores()
|
|
576
|
+
|
|
577
|
+
async def _compute_health_scores(self) -> None:
|
|
578
|
+
"""Score venv environments and populate the Health column."""
|
|
579
|
+
if not self.venv_rows:
|
|
580
|
+
return
|
|
581
|
+
envs = [row["environment"] for row in self.venv_rows]
|
|
582
|
+
scored = await asyncio.to_thread(lambda: score_all(envs, run_git=False))
|
|
583
|
+
engine = SuggestionEngine()
|
|
584
|
+
suggestions = engine.classify_all(scored)
|
|
585
|
+
for suggestion in suggestions:
|
|
586
|
+
self._health_by_path[str(suggestion.env_path)] = suggestion.category
|
|
587
|
+
for row in self.venv_rows:
|
|
588
|
+
row["health"] = self._health_by_path.get(row["path"], "")
|
|
589
|
+
self.render_venv_table()
|
|
548
590
|
|
|
549
591
|
async def action_clean_pycache(self):
|
|
550
592
|
total_freed_space = await asyncio.to_thread(remove_pycache, self.root_dir)
|
|
@@ -612,11 +654,12 @@ class TableApp(App):
|
|
|
612
654
|
return
|
|
613
655
|
elif current_status == EnvStatus.MARKED_TO_DELETE.value:
|
|
614
656
|
row["status"] = ""
|
|
615
|
-
table.update_cell_at((cursor_cell.row,
|
|
657
|
+
table.update_cell_at((cursor_cell.row, self.VENV_COL_STATUS), "")
|
|
616
658
|
else:
|
|
617
659
|
row["status"] = EnvStatus.MARKED_TO_DELETE.value
|
|
618
660
|
table.update_cell_at(
|
|
619
|
-
(cursor_cell.row,
|
|
661
|
+
(cursor_cell.row, self.VENV_COL_STATUS),
|
|
662
|
+
EnvStatus.MARKED_TO_DELETE.value,
|
|
620
663
|
)
|
|
621
664
|
|
|
622
665
|
@is_venv_tab
|
|
@@ -633,7 +676,10 @@ class TableApp(App):
|
|
|
633
676
|
if self.delete_environment(row["environment"]):
|
|
634
677
|
self.bytes_release += int(row["size"])
|
|
635
678
|
row["status"] = EnvStatus.DELETED.value
|
|
636
|
-
table.update_cell_at(
|
|
679
|
+
table.update_cell_at(
|
|
680
|
+
(cursor_cell.row, self.VENV_COL_STATUS),
|
|
681
|
+
EnvStatus.DELETED.value,
|
|
682
|
+
)
|
|
637
683
|
self.query_one("#status-label", Label).update(
|
|
638
684
|
f"{format_size(self.bytes_release)} deleted"
|
|
639
685
|
)
|
|
@@ -773,7 +819,8 @@ class TableApp(App):
|
|
|
773
819
|
self._selected_venv_indices.add(data_index)
|
|
774
820
|
self._update_multi_select_label()
|
|
775
821
|
table.update_cell_at(
|
|
776
|
-
(cursor_cell.row,
|
|
822
|
+
(cursor_cell.row, self.VENV_COL_STATUS),
|
|
823
|
+
self._compute_row_status(data_index, row),
|
|
777
824
|
)
|
|
778
825
|
|
|
779
826
|
@is_venv_tab
|
|
@@ -11,6 +11,7 @@ from rich.console import Console
|
|
|
11
11
|
|
|
12
12
|
from killpy.cleaner import Cleaner, CleanerError
|
|
13
13
|
from killpy.files import format_size
|
|
14
|
+
from killpy.intelligence.tracker import UsageTracker
|
|
14
15
|
from killpy.models import Environment
|
|
15
16
|
from killpy.scanner import Scanner
|
|
16
17
|
|
|
@@ -29,9 +30,7 @@ def _filter_envs(
|
|
|
29
30
|
|
|
30
31
|
if older_than is not None:
|
|
31
32
|
cutoff = now - timedelta(days=older_than)
|
|
32
|
-
result = [
|
|
33
|
-
e for e in result if e.last_accessed < cutoff
|
|
34
|
-
]
|
|
33
|
+
result = [e for e in result if e.last_accessed < cutoff]
|
|
35
34
|
|
|
36
35
|
return result
|
|
37
36
|
|
|
@@ -135,5 +134,11 @@ def delete_cmd(
|
|
|
135
134
|
+ (f" — [red]{errors} error(s)[/red]" if errors else "")
|
|
136
135
|
)
|
|
137
136
|
|
|
137
|
+
# Best-effort: record deletion in history.
|
|
138
|
+
try:
|
|
139
|
+
UsageTracker().record_deletion(freed)
|
|
140
|
+
except Exception: # noqa: BLE001
|
|
141
|
+
pass
|
|
142
|
+
|
|
138
143
|
if errors:
|
|
139
144
|
sys.exit(1)
|