killpy 0.25.1__tar.gz → 1.0.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.25.1 → killpy-1.0.0}/PKG-INFO +9 -10
- {killpy-0.25.1 → killpy-1.0.0}/README.md +8 -9
- {killpy-0.25.1 → killpy-1.0.0}/killpy/cli.py +5 -9
- {killpy-0.25.1 → killpy-1.0.0}/killpy/commands/_utils.py +2 -2
- {killpy-0.25.1 → killpy-1.0.0}/killpy/commands/delete.py +1 -1
- {killpy-0.25.1 → killpy-1.0.0}/killpy/commands/doctor.py +2 -2
- {killpy-0.25.1 → killpy-1.0.0}/killpy/commands/list.py +3 -3
- {killpy-0.25.1 → killpy-1.0.0}/killpy/detectors/artifacts.py +1 -1
- {killpy-0.25.1 → killpy-1.0.0}/killpy/detectors/cache.py +1 -1
- {killpy-0.25.1 → killpy-1.0.0}/killpy/detectors/conda.py +1 -1
- {killpy-0.25.1 → killpy-1.0.0}/killpy/detectors/hatch.py +1 -1
- {killpy-0.25.1 → killpy-1.0.0}/killpy/detectors/pipenv.py +1 -1
- {killpy-0.25.1 → killpy-1.0.0}/killpy/detectors/pipx.py +1 -1
- {killpy-0.25.1 → killpy-1.0.0}/killpy/detectors/poetry.py +1 -1
- {killpy-0.25.1 → killpy-1.0.0}/killpy/detectors/pyenv.py +1 -1
- {killpy-0.25.1 → killpy-1.0.0}/killpy/detectors/tox.py +1 -1
- {killpy-0.25.1 → killpy-1.0.0}/killpy/detectors/uv.py +1 -1
- {killpy-0.25.1 → killpy-1.0.0}/killpy/detectors/venv.py +1 -1
- {killpy-0.25.1 → killpy-1.0.0}/killpy/intelligence/scoring.py +4 -4
- {killpy-0.25.1 → killpy-1.0.0}/killpy/intelligence/suggestions.py +21 -25
- {killpy-0.25.1 → killpy-1.0.0}/killpy/models.py +7 -6
- {killpy-0.25.1 → killpy-1.0.0}/killpy.egg-info/PKG-INFO +9 -10
- {killpy-0.25.1 → killpy-1.0.0}/pyproject.toml +1 -1
- {killpy-0.25.1 → killpy-1.0.0}/killpy/__init__.py +0 -0
- {killpy-0.25.1 → killpy-1.0.0}/killpy/__main__.py +0 -0
- {killpy-0.25.1 → killpy-1.0.0}/killpy/cleaner.py +0 -0
- {killpy-0.25.1 → killpy-1.0.0}/killpy/cleaners/__init__.py +0 -0
- {killpy-0.25.1 → killpy-1.0.0}/killpy/commands/__init__.py +0 -0
- {killpy-0.25.1 → killpy-1.0.0}/killpy/commands/clean.py +0 -0
- {killpy-0.25.1 → killpy-1.0.0}/killpy/commands/find.py +0 -0
- {killpy-0.25.1 → killpy-1.0.0}/killpy/commands/stats.py +0 -0
- {killpy-0.25.1 → killpy-1.0.0}/killpy/detectors/__init__.py +0 -0
- {killpy-0.25.1 → killpy-1.0.0}/killpy/detectors/base.py +0 -0
- {killpy-0.25.1 → killpy-1.0.0}/killpy/files/__init__.py +0 -0
- {killpy-0.25.1 → killpy-1.0.0}/killpy/intelligence/__init__.py +0 -0
- {killpy-0.25.1 → killpy-1.0.0}/killpy/intelligence/git_analyzer.py +0 -0
- {killpy-0.25.1 → killpy-1.0.0}/killpy/intelligence/tracker.py +0 -0
- {killpy-0.25.1 → killpy-1.0.0}/killpy/scanner.py +0 -0
- {killpy-0.25.1 → killpy-1.0.0}/killpy.egg-info/SOURCES.txt +0 -0
- {killpy-0.25.1 → killpy-1.0.0}/killpy.egg-info/dependency_links.txt +0 -0
- {killpy-0.25.1 → killpy-1.0.0}/killpy.egg-info/entry_points.txt +0 -0
- {killpy-0.25.1 → killpy-1.0.0}/killpy.egg-info/requires.txt +0 -0
- {killpy-0.25.1 → killpy-1.0.0}/killpy.egg-info/top_level.txt +0 -0
- {killpy-0.25.1 → killpy-1.0.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: killpy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Find and delete unused Python environments (.venv, conda, poetry, pipenv, pipx, pyenv, uv) and caches to free disk space — from a fast terminal UI or a single CLI command.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Tlaloc-Es/killpy
|
|
6
6
|
Project-URL: Documentation, https://tlaloc-es.github.io/killpy/
|
|
@@ -196,7 +196,7 @@ Environments flagged with `⚠️` are actively in use (the environment killpy r
|
|
|
196
196
|
| Key | Action |
|
|
197
197
|
|-----|--------|
|
|
198
198
|
| `↑` / `↓` or `k` / `j` | Move cursor up / down (vim-style) |
|
|
199
|
-
| `/` | Open live search/filter bar (
|
|
199
|
+
| `/` | Open live search/filter bar (substring match) |
|
|
200
200
|
| `Escape` | Close search bar and clear filter |
|
|
201
201
|
| `T` | Toggle multi-select mode on / off |
|
|
202
202
|
| `Space` | *(Multi-select)* Toggle current row selected |
|
|
@@ -211,7 +211,7 @@ Environments flagged with `⚠️` are actively in use (the environment killpy r
|
|
|
211
211
|
|
|
212
212
|
### Search / filter
|
|
213
213
|
|
|
214
|
-
Press `/` to open the filter bar at the bottom of the screen. Type any
|
|
214
|
+
Press `/` to open the filter bar at the bottom of the screen. Type any text — the venv table filters by path substring (case-insensitive) and updates live as you type. Press `Escape` or submit an empty value to clear the filter and return to the full list.
|
|
215
215
|
|
|
216
216
|
### Multi-select mode
|
|
217
217
|
|
|
@@ -265,7 +265,7 @@ ______________________________________________________________________
|
|
|
265
265
|
killpy list # list all detected environments
|
|
266
266
|
killpy list --path ~/projects # scan a specific path
|
|
267
267
|
killpy list --type venv --type conda # filter by type (repeatable)
|
|
268
|
-
killpy list --older-than 90 # not
|
|
268
|
+
killpy list --older-than 90 # not modified in the last 90 days
|
|
269
269
|
killpy list --json # output as a JSON array
|
|
270
270
|
killpy list --json-stream # stream as NDJSON — one line per env
|
|
271
271
|
killpy list --quiet # suppress progress output (scripts/CI)
|
|
@@ -284,7 +284,7 @@ The **Path** column mirrors the form of `--path`: a relative `--path` produces r
|
|
|
284
284
|
"absolute_path": "/home/user/projects/my-app/.venv",
|
|
285
285
|
"name": "my-app/.venv",
|
|
286
286
|
"type": "venv",
|
|
287
|
-
"
|
|
287
|
+
"last_modified": "2025-11-02T14:23:01+00:00",
|
|
288
288
|
"size_bytes": 54393984,
|
|
289
289
|
"size_human": "51.88 MB",
|
|
290
290
|
"managed_by": null,
|
|
@@ -417,8 +417,7 @@ Category is assigned deterministically by applying the following rules in order:
|
|
|
417
417
|
|----------|------|----------|
|
|
418
418
|
| 1 | Orphan (`is_orphan=True`) **and** `age ≥ 180 days` | `HIGH` |
|
|
419
419
|
| 2 | Active git repository **or** `age < 120 days` | `LOW` |
|
|
420
|
-
| 3 | `age ≥ 120 days` | `MEDIUM` |
|
|
421
|
-
| 4 | Fallback | `LOW` |
|
|
420
|
+
| 3 | `age ≥ 120 days` *(exhaustive fallback)* | `MEDIUM` |
|
|
422
421
|
|
|
423
422
|
Age and orphan status are the dominant signals. Size does not affect the category.
|
|
424
423
|
|
|
@@ -498,7 +497,7 @@ Yes. Run `killpy --path ~` to scan your home folder. The `stats` command gives a
|
|
|
498
497
|
killpy delete --type venv --older-than 90 --yes
|
|
499
498
|
```
|
|
500
499
|
|
|
501
|
-
Deletes every `.venv` / `pyvenv.cfg` env not
|
|
500
|
+
Deletes every `.venv` / `pyvenv.cfg` env not modified in the last 90 days, without prompting.
|
|
502
501
|
|
|
503
502
|
**How do I use killpy in a CI pipeline or script?**
|
|
504
503
|
|
|
@@ -604,7 +603,7 @@ ______________________________________________________________________
|
|
|
604
603
|
|
|
605
604
|
```yaml
|
|
606
605
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
607
|
-
rev: 0.
|
|
606
|
+
rev: 1.0.0
|
|
608
607
|
hooks:
|
|
609
608
|
- id: killpy # remove __pycache__ on every commit
|
|
610
609
|
- id: killpy-clean-caches # also removes .mypy_cache, .pytest_cache, .ruff_cache
|
|
@@ -629,7 +628,7 @@ Typical minimal setup (safe for daily use):
|
|
|
629
628
|
|
|
630
629
|
```yaml
|
|
631
630
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
632
|
-
rev: 0.
|
|
631
|
+
rev: 1.0.0
|
|
633
632
|
hooks:
|
|
634
633
|
- id: killpy
|
|
635
634
|
```
|
|
@@ -167,7 +167,7 @@ Environments flagged with `⚠️` are actively in use (the environment killpy r
|
|
|
167
167
|
| Key | Action |
|
|
168
168
|
|-----|--------|
|
|
169
169
|
| `↑` / `↓` or `k` / `j` | Move cursor up / down (vim-style) |
|
|
170
|
-
| `/` | Open live search/filter bar (
|
|
170
|
+
| `/` | Open live search/filter bar (substring match) |
|
|
171
171
|
| `Escape` | Close search bar and clear filter |
|
|
172
172
|
| `T` | Toggle multi-select mode on / off |
|
|
173
173
|
| `Space` | *(Multi-select)* Toggle current row selected |
|
|
@@ -182,7 +182,7 @@ Environments flagged with `⚠️` are actively in use (the environment killpy r
|
|
|
182
182
|
|
|
183
183
|
### Search / filter
|
|
184
184
|
|
|
185
|
-
Press `/` to open the filter bar at the bottom of the screen. Type any
|
|
185
|
+
Press `/` to open the filter bar at the bottom of the screen. Type any text — the venv table filters by path substring (case-insensitive) and updates live as you type. Press `Escape` or submit an empty value to clear the filter and return to the full list.
|
|
186
186
|
|
|
187
187
|
### Multi-select mode
|
|
188
188
|
|
|
@@ -236,7 +236,7 @@ ______________________________________________________________________
|
|
|
236
236
|
killpy list # list all detected environments
|
|
237
237
|
killpy list --path ~/projects # scan a specific path
|
|
238
238
|
killpy list --type venv --type conda # filter by type (repeatable)
|
|
239
|
-
killpy list --older-than 90 # not
|
|
239
|
+
killpy list --older-than 90 # not modified in the last 90 days
|
|
240
240
|
killpy list --json # output as a JSON array
|
|
241
241
|
killpy list --json-stream # stream as NDJSON — one line per env
|
|
242
242
|
killpy list --quiet # suppress progress output (scripts/CI)
|
|
@@ -255,7 +255,7 @@ The **Path** column mirrors the form of `--path`: a relative `--path` produces r
|
|
|
255
255
|
"absolute_path": "/home/user/projects/my-app/.venv",
|
|
256
256
|
"name": "my-app/.venv",
|
|
257
257
|
"type": "venv",
|
|
258
|
-
"
|
|
258
|
+
"last_modified": "2025-11-02T14:23:01+00:00",
|
|
259
259
|
"size_bytes": 54393984,
|
|
260
260
|
"size_human": "51.88 MB",
|
|
261
261
|
"managed_by": null,
|
|
@@ -388,8 +388,7 @@ Category is assigned deterministically by applying the following rules in order:
|
|
|
388
388
|
|----------|------|----------|
|
|
389
389
|
| 1 | Orphan (`is_orphan=True`) **and** `age ≥ 180 days` | `HIGH` |
|
|
390
390
|
| 2 | Active git repository **or** `age < 120 days` | `LOW` |
|
|
391
|
-
| 3 | `age ≥ 120 days` | `MEDIUM` |
|
|
392
|
-
| 4 | Fallback | `LOW` |
|
|
391
|
+
| 3 | `age ≥ 120 days` *(exhaustive fallback)* | `MEDIUM` |
|
|
393
392
|
|
|
394
393
|
Age and orphan status are the dominant signals. Size does not affect the category.
|
|
395
394
|
|
|
@@ -469,7 +468,7 @@ Yes. Run `killpy --path ~` to scan your home folder. The `stats` command gives a
|
|
|
469
468
|
killpy delete --type venv --older-than 90 --yes
|
|
470
469
|
```
|
|
471
470
|
|
|
472
|
-
Deletes every `.venv` / `pyvenv.cfg` env not
|
|
471
|
+
Deletes every `.venv` / `pyvenv.cfg` env not modified in the last 90 days, without prompting.
|
|
473
472
|
|
|
474
473
|
**How do I use killpy in a CI pipeline or script?**
|
|
475
474
|
|
|
@@ -575,7 +574,7 @@ ______________________________________________________________________
|
|
|
575
574
|
|
|
576
575
|
```yaml
|
|
577
576
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
578
|
-
rev: 0.
|
|
577
|
+
rev: 1.0.0
|
|
579
578
|
hooks:
|
|
580
579
|
- id: killpy # remove __pycache__ on every commit
|
|
581
580
|
- id: killpy-clean-caches # also removes .mypy_cache, .pytest_cache, .ruff_cache
|
|
@@ -600,7 +599,7 @@ Typical minimal setup (safe for daily use):
|
|
|
600
599
|
|
|
601
600
|
```yaml
|
|
602
601
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
603
|
-
rev: 0.
|
|
602
|
+
rev: 1.0.0
|
|
604
603
|
hooks:
|
|
605
604
|
- id: killpy
|
|
606
605
|
```
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
-
import re
|
|
7
6
|
import subprocess
|
|
8
7
|
import sys
|
|
9
8
|
from datetime import datetime
|
|
@@ -297,7 +296,7 @@ class TableApp(App):
|
|
|
297
296
|
yield Label("", id="status-label")
|
|
298
297
|
yield Label("", id="selected-path-label")
|
|
299
298
|
yield Input(
|
|
300
|
-
placeholder="Filter by path
|
|
299
|
+
placeholder="Filter by path… Esc to clear",
|
|
301
300
|
id="search-input",
|
|
302
301
|
)
|
|
303
302
|
yield Label("", id="multi-select-label")
|
|
@@ -337,7 +336,7 @@ class TableApp(App):
|
|
|
337
336
|
{
|
|
338
337
|
"path": str(environment.path),
|
|
339
338
|
"type": environment.type,
|
|
340
|
-
"last_modified": environment.
|
|
339
|
+
"last_modified": environment.last_modified_str,
|
|
341
340
|
"size": environment.size_bytes,
|
|
342
341
|
"size_human": environment.size_human,
|
|
343
342
|
"health": self._health_by_path.get(str(environment.path), ""),
|
|
@@ -382,13 +381,10 @@ class TableApp(App):
|
|
|
382
381
|
table.clear(columns=True)
|
|
383
382
|
table.add_columns(*self.get_headers_for_table("venv-table"))
|
|
384
383
|
self._venv_display_indices = []
|
|
384
|
+
query = self._filter_query.lower()
|
|
385
385
|
for i, row in enumerate(self.venv_rows):
|
|
386
|
-
if
|
|
387
|
-
|
|
388
|
-
if not re.search(self._filter_query, row["path"], re.IGNORECASE):
|
|
389
|
-
continue
|
|
390
|
-
except re.error:
|
|
391
|
-
pass # invalid regex — show all
|
|
386
|
+
if query and query not in row["path"].lower():
|
|
387
|
+
continue
|
|
392
388
|
self._venv_display_indices.append(i)
|
|
393
389
|
env = row["environment"]
|
|
394
390
|
type_label = ("\u26a0\ufe0f " if env.is_system_critical else "") + row[
|
|
@@ -68,7 +68,7 @@ def filter_envs(
|
|
|
68
68
|
names such as ``"venv"`` and ``"cache"`` are automatically expanded to
|
|
69
69
|
their concrete sub-type values via :data:`_TYPE_ALIASES`.
|
|
70
70
|
older_than:
|
|
71
|
-
If provided, only environments not
|
|
71
|
+
If provided, only environments not modified in the last *older_than* days
|
|
72
72
|
are kept.
|
|
73
73
|
"""
|
|
74
74
|
now = datetime.now(tz=timezone.utc)
|
|
@@ -84,6 +84,6 @@ def filter_envs(
|
|
|
84
84
|
|
|
85
85
|
if older_than is not None:
|
|
86
86
|
cutoff = now - timedelta(days=older_than)
|
|
87
|
-
result = [e for e in result if e.
|
|
87
|
+
result = [e for e in result if e.last_modified < cutoff]
|
|
88
88
|
|
|
89
89
|
return result
|
|
@@ -37,7 +37,7 @@ from killpy.scanner import Scanner
|
|
|
37
37
|
type=int,
|
|
38
38
|
default=None,
|
|
39
39
|
metavar="DAYS",
|
|
40
|
-
help="Only delete environments not
|
|
40
|
+
help="Only delete environments not modified in the last N days.",
|
|
41
41
|
)
|
|
42
42
|
@click.option(
|
|
43
43
|
"--dry-run",
|
|
@@ -195,7 +195,7 @@ def _output_rich(
|
|
|
195
195
|
|
|
196
196
|
now = datetime.now(tz=timezone.utc)
|
|
197
197
|
for se in top:
|
|
198
|
-
la = se.env.
|
|
198
|
+
la = se.env.last_modified
|
|
199
199
|
if la.tzinfo is None:
|
|
200
200
|
la = la.replace(tzinfo=timezone.utc)
|
|
201
201
|
age_days = (now - la).days
|
|
@@ -264,7 +264,7 @@ def _print_category_table(
|
|
|
264
264
|
|
|
265
265
|
now = datetime.now(tz=timezone.utc)
|
|
266
266
|
for se in scored_envs:
|
|
267
|
-
la = se.env.
|
|
267
|
+
la = se.env.last_modified
|
|
268
268
|
if la.tzinfo is None:
|
|
269
269
|
la = la.replace(tzinfo=timezone.utc)
|
|
270
270
|
age_days = (now - la).days
|
|
@@ -58,7 +58,7 @@ def _print_table(envs: list, console: Console) -> None:
|
|
|
58
58
|
table = Table(show_header=True, header_style="bold cyan")
|
|
59
59
|
table.add_column("Type", style="dim", min_width=10)
|
|
60
60
|
table.add_column("Name", min_width=20)
|
|
61
|
-
table.add_column("Last
|
|
61
|
+
table.add_column("Last modified", min_width=12)
|
|
62
62
|
table.add_column("Size", justify="right", min_width=9)
|
|
63
63
|
table.add_column("Path")
|
|
64
64
|
|
|
@@ -66,7 +66,7 @@ def _print_table(envs: list, console: Console) -> None:
|
|
|
66
66
|
table.add_row(
|
|
67
67
|
env.type,
|
|
68
68
|
env.name,
|
|
69
|
-
env.
|
|
69
|
+
env.last_modified_str,
|
|
70
70
|
env.size_human,
|
|
71
71
|
str(env.path),
|
|
72
72
|
)
|
|
@@ -101,7 +101,7 @@ def _print_table(envs: list, console: Console) -> None:
|
|
|
101
101
|
type=int,
|
|
102
102
|
default=None,
|
|
103
103
|
metavar="DAYS",
|
|
104
|
-
help="Only show environments not
|
|
104
|
+
help="Only show environments not modified in the last N days.",
|
|
105
105
|
)
|
|
106
106
|
@click.option(
|
|
107
107
|
"--json-stream",
|
|
@@ -69,8 +69,8 @@ class ScoringService:
|
|
|
69
69
|
size_score = self._normalize_size(env.size_bytes)
|
|
70
70
|
explanation.append(f"Size: {env.size_human}")
|
|
71
71
|
|
|
72
|
-
age_score, age_days = self._normalize_age(env.
|
|
73
|
-
explanation.append(f"Last
|
|
72
|
+
age_score, age_days = self._normalize_age(env.last_modified)
|
|
73
|
+
explanation.append(f"Last modified {age_days} days ago")
|
|
74
74
|
|
|
75
75
|
is_orphan, orphan_score = self._orphan_score(env.path)
|
|
76
76
|
has_project_files = not is_orphan
|
|
@@ -132,11 +132,11 @@ class ScoringService:
|
|
|
132
132
|
return 1.0 / (1.0 + math.exp(-k * (size_bytes - x0)))
|
|
133
133
|
|
|
134
134
|
@staticmethod
|
|
135
|
-
def _normalize_age(
|
|
135
|
+
def _normalize_age(last_modified: datetime) -> tuple[float, int]:
|
|
136
136
|
"""Return (age_score, age_days) where age_score is linear [0, 1]."""
|
|
137
137
|
now = datetime.now(tz=timezone.utc)
|
|
138
138
|
try:
|
|
139
|
-
la =
|
|
139
|
+
la = last_modified
|
|
140
140
|
if la.tzinfo is None:
|
|
141
141
|
la = la.replace(tzinfo=timezone.utc)
|
|
142
142
|
age_days = max(0, (now - la).days)
|
|
@@ -9,8 +9,9 @@ from killpy.models import ScoredEnvironment, Suggestion
|
|
|
9
9
|
|
|
10
10
|
# Age thresholds in days.
|
|
11
11
|
_HIGH_AGE_ORPHAN_THRESHOLD = 180 # orphan + age >= this → HIGH
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
# Single LOW/MEDIUM cutoff: age < this → LOW (recently modified), age >= this →
|
|
13
|
+
# MEDIUM (stale). Active git always forces LOW regardless of age.
|
|
14
|
+
_STALE_AGE_THRESHOLD = 120
|
|
14
15
|
|
|
15
16
|
_ACTION_HIGH = "Delete — unused and orphaned"
|
|
16
17
|
_ACTION_MEDIUM = "Review — possibly unused"
|
|
@@ -59,8 +60,8 @@ class SuggestionEngine:
|
|
|
59
60
|
|
|
60
61
|
@staticmethod
|
|
61
62
|
def _age_days(scored: ScoredEnvironment) -> int:
|
|
62
|
-
"""Return age in days based on env.
|
|
63
|
-
la = scored.env.
|
|
63
|
+
"""Return age in days based on env.last_modified."""
|
|
64
|
+
la = scored.env.last_modified
|
|
64
65
|
if la.tzinfo is None:
|
|
65
66
|
la = la.replace(tzinfo=timezone.utc)
|
|
66
67
|
return max(0, (datetime.now(tz=timezone.utc) - la).days)
|
|
@@ -81,32 +82,27 @@ class SuggestionEngine:
|
|
|
81
82
|
# ── Rule 1: HIGH — orphan and stale ────────────────────────────────────
|
|
82
83
|
if is_orphan and age_days >= _HIGH_AGE_ORPHAN_THRESHOLD:
|
|
83
84
|
reasons.append("Orphan — no project files found nearby")
|
|
84
|
-
reasons.append(f"Not
|
|
85
|
+
reasons.append(f"Not modified in {age_days} days")
|
|
85
86
|
return "HIGH", _ACTION_HIGH
|
|
86
87
|
|
|
87
88
|
# ── Rule 2: LOW — active git or very recent ─────────────────────────────
|
|
88
|
-
if is_active_git or age_days <
|
|
89
|
+
if is_active_git or age_days < _STALE_AGE_THRESHOLD:
|
|
89
90
|
if is_active_git:
|
|
90
91
|
reasons.append("Active git repository")
|
|
91
|
-
if age_days <
|
|
92
|
-
reasons.append(f"Recently
|
|
92
|
+
if age_days < _STALE_AGE_THRESHOLD:
|
|
93
|
+
reasons.append(f"Recently modified ({age_days} days ago)")
|
|
93
94
|
action = _ACTION_LOW_ACTIVE if is_active_git else _ACTION_LOW
|
|
94
95
|
return "LOW", action
|
|
95
96
|
|
|
96
|
-
# ── Rule 3: MEDIUM —
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
# ── Rule 4: FALLBACK → LOW ──────────────────────────────────────────────
|
|
110
|
-
if is_active_git:
|
|
111
|
-
reasons.append("Active git repository")
|
|
112
|
-
return "LOW", _ACTION_LOW
|
|
97
|
+
# ── Rule 3: MEDIUM — stale (not active, age >= _STALE_AGE_THRESHOLD) ────
|
|
98
|
+
# Exhaustive fallback: everything not caught by Rules 1–2 is stale.
|
|
99
|
+
if is_orphan:
|
|
100
|
+
reasons.append("Orphan — no project files found nearby")
|
|
101
|
+
reasons.append(f"Not modified in {age_days} days")
|
|
102
|
+
if (
|
|
103
|
+
scored.git_info is not None
|
|
104
|
+
and scored.git_info.is_git_repo
|
|
105
|
+
and not scored.git_info.is_active
|
|
106
|
+
):
|
|
107
|
+
reasons.append("Associated git repo has no recent commits")
|
|
108
|
+
return "MEDIUM", _ACTION_MEDIUM
|
|
@@ -35,8 +35,9 @@ class Environment:
|
|
|
35
35
|
type:
|
|
36
36
|
Short detector tag, e.g. ``"venv"``, ``"pyvenv.cfg"``, ``"poetry"``,
|
|
37
37
|
``"conda"``, ``"pipx"``, ``"cache"``, ``"uv"``, ``"artifacts"``…
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
last_modified:
|
|
39
|
+
Last modification time (``st_mtime``) of the environment root, as
|
|
40
|
+
reported by the filesystem — not an access time.
|
|
40
41
|
size_bytes:
|
|
41
42
|
Total size in bytes (recursive directory sum).
|
|
42
43
|
managed_by:
|
|
@@ -48,7 +49,7 @@ class Environment:
|
|
|
48
49
|
path: Path
|
|
49
50
|
name: str
|
|
50
51
|
type: str
|
|
51
|
-
|
|
52
|
+
last_modified: datetime
|
|
52
53
|
size_bytes: int
|
|
53
54
|
managed_by: str | None = None
|
|
54
55
|
is_system_critical: bool = False
|
|
@@ -63,9 +64,9 @@ class Environment:
|
|
|
63
64
|
return format_size(self.size_bytes)
|
|
64
65
|
|
|
65
66
|
@property
|
|
66
|
-
def
|
|
67
|
+
def last_modified_str(self) -> str:
|
|
67
68
|
"""Formatted date string ``DD/MM/YYYY`` for display."""
|
|
68
|
-
return self.
|
|
69
|
+
return self.last_modified.strftime("%d/%m/%Y")
|
|
69
70
|
|
|
70
71
|
# ------------------------------------------------------------------ #
|
|
71
72
|
# Serialisation #
|
|
@@ -83,7 +84,7 @@ class Environment:
|
|
|
83
84
|
"absolute_path": str(abs_path),
|
|
84
85
|
"name": self.name,
|
|
85
86
|
"type": self.type,
|
|
86
|
-
"
|
|
87
|
+
"last_modified": self.last_modified.isoformat(),
|
|
87
88
|
"size_bytes": self.size_bytes,
|
|
88
89
|
"size_human": self.size_human,
|
|
89
90
|
"managed_by": self.managed_by,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: killpy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Find and delete unused Python environments (.venv, conda, poetry, pipenv, pipx, pyenv, uv) and caches to free disk space — from a fast terminal UI or a single CLI command.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Tlaloc-Es/killpy
|
|
6
6
|
Project-URL: Documentation, https://tlaloc-es.github.io/killpy/
|
|
@@ -196,7 +196,7 @@ Environments flagged with `⚠️` are actively in use (the environment killpy r
|
|
|
196
196
|
| Key | Action |
|
|
197
197
|
|-----|--------|
|
|
198
198
|
| `↑` / `↓` or `k` / `j` | Move cursor up / down (vim-style) |
|
|
199
|
-
| `/` | Open live search/filter bar (
|
|
199
|
+
| `/` | Open live search/filter bar (substring match) |
|
|
200
200
|
| `Escape` | Close search bar and clear filter |
|
|
201
201
|
| `T` | Toggle multi-select mode on / off |
|
|
202
202
|
| `Space` | *(Multi-select)* Toggle current row selected |
|
|
@@ -211,7 +211,7 @@ Environments flagged with `⚠️` are actively in use (the environment killpy r
|
|
|
211
211
|
|
|
212
212
|
### Search / filter
|
|
213
213
|
|
|
214
|
-
Press `/` to open the filter bar at the bottom of the screen. Type any
|
|
214
|
+
Press `/` to open the filter bar at the bottom of the screen. Type any text — the venv table filters by path substring (case-insensitive) and updates live as you type. Press `Escape` or submit an empty value to clear the filter and return to the full list.
|
|
215
215
|
|
|
216
216
|
### Multi-select mode
|
|
217
217
|
|
|
@@ -265,7 +265,7 @@ ______________________________________________________________________
|
|
|
265
265
|
killpy list # list all detected environments
|
|
266
266
|
killpy list --path ~/projects # scan a specific path
|
|
267
267
|
killpy list --type venv --type conda # filter by type (repeatable)
|
|
268
|
-
killpy list --older-than 90 # not
|
|
268
|
+
killpy list --older-than 90 # not modified in the last 90 days
|
|
269
269
|
killpy list --json # output as a JSON array
|
|
270
270
|
killpy list --json-stream # stream as NDJSON — one line per env
|
|
271
271
|
killpy list --quiet # suppress progress output (scripts/CI)
|
|
@@ -284,7 +284,7 @@ The **Path** column mirrors the form of `--path`: a relative `--path` produces r
|
|
|
284
284
|
"absolute_path": "/home/user/projects/my-app/.venv",
|
|
285
285
|
"name": "my-app/.venv",
|
|
286
286
|
"type": "venv",
|
|
287
|
-
"
|
|
287
|
+
"last_modified": "2025-11-02T14:23:01+00:00",
|
|
288
288
|
"size_bytes": 54393984,
|
|
289
289
|
"size_human": "51.88 MB",
|
|
290
290
|
"managed_by": null,
|
|
@@ -417,8 +417,7 @@ Category is assigned deterministically by applying the following rules in order:
|
|
|
417
417
|
|----------|------|----------|
|
|
418
418
|
| 1 | Orphan (`is_orphan=True`) **and** `age ≥ 180 days` | `HIGH` |
|
|
419
419
|
| 2 | Active git repository **or** `age < 120 days` | `LOW` |
|
|
420
|
-
| 3 | `age ≥ 120 days` | `MEDIUM` |
|
|
421
|
-
| 4 | Fallback | `LOW` |
|
|
420
|
+
| 3 | `age ≥ 120 days` *(exhaustive fallback)* | `MEDIUM` |
|
|
422
421
|
|
|
423
422
|
Age and orphan status are the dominant signals. Size does not affect the category.
|
|
424
423
|
|
|
@@ -498,7 +497,7 @@ Yes. Run `killpy --path ~` to scan your home folder. The `stats` command gives a
|
|
|
498
497
|
killpy delete --type venv --older-than 90 --yes
|
|
499
498
|
```
|
|
500
499
|
|
|
501
|
-
Deletes every `.venv` / `pyvenv.cfg` env not
|
|
500
|
+
Deletes every `.venv` / `pyvenv.cfg` env not modified in the last 90 days, without prompting.
|
|
502
501
|
|
|
503
502
|
**How do I use killpy in a CI pipeline or script?**
|
|
504
503
|
|
|
@@ -604,7 +603,7 @@ ______________________________________________________________________
|
|
|
604
603
|
|
|
605
604
|
```yaml
|
|
606
605
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
607
|
-
rev: 0.
|
|
606
|
+
rev: 1.0.0
|
|
608
607
|
hooks:
|
|
609
608
|
- id: killpy # remove __pycache__ on every commit
|
|
610
609
|
- id: killpy-clean-caches # also removes .mypy_cache, .pytest_cache, .ruff_cache
|
|
@@ -629,7 +628,7 @@ Typical minimal setup (safe for daily use):
|
|
|
629
628
|
|
|
630
629
|
```yaml
|
|
631
630
|
- repo: https://github.com/Tlaloc-Es/KillPy
|
|
632
|
-
rev: 0.
|
|
631
|
+
rev: 1.0.0
|
|
633
632
|
hooks:
|
|
634
633
|
- id: killpy
|
|
635
634
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "killpy"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "1.0.0"
|
|
4
4
|
description = "Find and delete unused Python environments (.venv, conda, poetry, pipenv, pipx, pyenv, uv) and caches to free disk space — from a fast terminal UI or a single CLI command."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
keywords = [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|