dsz 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.
dsz-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.3
2
+ Name: dsz
3
+ Version: 0.1.0
4
+ Summary: Show disk usage of a directory's immediate children.
5
+ Author: Kyle O'Malley
6
+ Author-email: Kyle O'Malley <j.kyle.omalley@gmail.com>
7
+ Requires-Python: >=3.14
8
+ Description-Content-Type: text/markdown
9
+
10
+ # dsz
11
+
12
+ Show disk usage of a directory's immediate children, sorted by size.
13
+
14
+ ## Installation
15
+
16
+ ```
17
+ uv tool install dsz
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```
23
+ dsz [PATH] [--min-percent N]
24
+ ```
25
+
26
+ - `PATH` — directory to scan (default: current directory)
27
+ - `--min-percent N` — collapse entries below N% of the total into a single `<other>` line (default: 1.0)
28
+
29
+ ## Example
30
+
31
+ ```
32
+ $ dsz ~
33
+ /home/user 24.2 GB
34
+ 18.2 GB 75% Videos ###############
35
+ 4.1 GB 17% Documents ###
36
+ 1.6 GB 7% Downloads #
37
+ 312.4 MB 1% <other 5>
38
+ ```
39
+
40
+ Hidden entries (dot files and directories) and symlinks are excluded from all counts. Drill into a subdirectory by passing it as `PATH`.
dsz-0.1.0/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # dsz
2
+
3
+ Show disk usage of a directory's immediate children, sorted by size.
4
+
5
+ ## Installation
6
+
7
+ ```
8
+ uv tool install dsz
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```
14
+ dsz [PATH] [--min-percent N]
15
+ ```
16
+
17
+ - `PATH` — directory to scan (default: current directory)
18
+ - `--min-percent N` — collapse entries below N% of the total into a single `<other>` line (default: 1.0)
19
+
20
+ ## Example
21
+
22
+ ```
23
+ $ dsz ~
24
+ /home/user 24.2 GB
25
+ 18.2 GB 75% Videos ###############
26
+ 4.1 GB 17% Documents ###
27
+ 1.6 GB 7% Downloads #
28
+ 312.4 MB 1% <other 5>
29
+ ```
30
+
31
+ Hidden entries (dot files and directories) and symlinks are excluded from all counts. Drill into a subdirectory by passing it as `PATH`.
@@ -0,0 +1,15 @@
1
+ [project]
2
+ name = "dsz"
3
+ version = "0.1.0"
4
+ description = "Show disk usage of a directory's immediate children."
5
+ readme = "README.md"
6
+ authors = [{ name = "Kyle O'Malley", email = "j.kyle.omalley@gmail.com" }]
7
+ requires-python = ">=3.14"
8
+ dependencies = []
9
+
10
+ [project.scripts]
11
+ dsz = "dsz.cli:main"
12
+
13
+ [build-system]
14
+ requires = ["uv_build>=0.11.16,<0.12.0"]
15
+ build-backend = "uv_build"
@@ -0,0 +1 @@
1
+ """Show disk usage of a directory's immediate children."""
@@ -0,0 +1,36 @@
1
+ from dsz.core import generate_size_report
2
+
3
+ from pathlib import Path
4
+
5
+ import argparse
6
+ import sys
7
+
8
+
9
+ def main() -> None:
10
+ parser = argparse.ArgumentParser(description="Show disk usage of a directory's immediate children.")
11
+ parser.add_argument(
12
+ "PATH",
13
+ nargs="?",
14
+ default=".",
15
+ type=Path,
16
+ help="directory to scan (default: current directory)",
17
+ )
18
+ parser.add_argument(
19
+ "--min-percent",
20
+ default=1.0,
21
+ metavar="N",
22
+ type=float,
23
+ help="collapse entries below N%% of total into one '<other>' line (default: 1.0)",
24
+ )
25
+ args = parser.parse_args()
26
+
27
+ directory: Path = args.PATH
28
+ min_percent: float = args.min_percent
29
+
30
+ try:
31
+ results = generate_size_report(directory, min_percent)
32
+ except ValueError as e:
33
+ print(f"error: {e}", file=sys.stderr)
34
+ raise SystemExit(1)
35
+
36
+ print(results, end="")
@@ -0,0 +1,111 @@
1
+ """dsz scan module."""
2
+
3
+ from pathlib import Path
4
+ import os
5
+
6
+
7
+ def _fmt_size(n: int) -> str:
8
+ if n < 1024:
9
+ return f"{n} B"
10
+
11
+ size = float(n)
12
+
13
+ for unit in ("KB", "MB", "GB"):
14
+ size /= 1024
15
+ if size < 1024 or unit == "GB":
16
+ break
17
+
18
+ return f"{size:.1f} {unit}"
19
+
20
+
21
+ def _dir_size(path: str) -> int:
22
+ """Return total byte size of all files under path, skipping symlinks and permission-denied entries."""
23
+ total_size = 0
24
+ stack = [path]
25
+
26
+ while stack:
27
+ current = stack.pop()
28
+
29
+ try:
30
+ scandir_ctx = os.scandir(current)
31
+ except PermissionError:
32
+ continue
33
+
34
+ with scandir_ctx as dir_iter:
35
+ for entry in dir_iter:
36
+ if entry.is_symlink():
37
+ continue
38
+ try:
39
+ entry_stat = entry.stat()
40
+ except OSError:
41
+ continue
42
+ if entry.is_file(follow_symlinks=False):
43
+ total_size += entry_stat.st_size
44
+ elif entry.is_dir(follow_symlinks=False):
45
+ stack.append(entry.path)
46
+
47
+ return total_size
48
+
49
+
50
+ def generate_size_report(path: Path, min_percent: float) -> str:
51
+ """Scan path's immediate children and return a formatted size report string.
52
+
53
+ Raises ValueError if path does not exist or is not a directory.
54
+ Hidden entries (leading dot) and symlinks are excluded from all counts.
55
+ Entries below min_percent of the total are collapsed into a single '<other N>' line.
56
+ """
57
+ if not path.exists():
58
+ msg = f"path '{path}' does not exist"
59
+ raise ValueError(msg)
60
+
61
+ if not path.is_dir():
62
+ msg = f"path '{path}' is not a directory"
63
+ raise ValueError(msg)
64
+
65
+ total_size: int = 0
66
+
67
+ entries: dict[str, int] = {}
68
+
69
+ try:
70
+ with os.scandir(path) as dir_iter:
71
+ for entry in dir_iter:
72
+ if entry.name.startswith("."):
73
+ continue
74
+ if entry.is_symlink():
75
+ continue
76
+ entry_stat = entry.stat()
77
+ if entry.is_file(follow_symlinks=False):
78
+ total_size += entry_stat.st_size
79
+ entries[entry.name] = entry_stat.st_size
80
+ elif entry.is_dir(follow_symlinks=False):
81
+ entry_size = _dir_size(entry.path)
82
+ total_size += entry_size
83
+ entries[entry.name] = entry_size
84
+ except PermissionError:
85
+ pass # unreadable directory: return header with whatever was counted
86
+
87
+ out = f"{path.absolute()} {_fmt_size(total_size)}\n"
88
+
89
+ if not entries or total_size == 0:
90
+ return out
91
+
92
+ sorted_entries = sorted(entries.items(), key=lambda x: -x[-1])
93
+
94
+ above, below = [], []
95
+
96
+ for name, size in sorted_entries:
97
+ percent = (size / total_size) * 100
98
+ (above if percent >= min_percent else below).append((name, size, percent))
99
+
100
+ for entry, size, percent in above:
101
+ bar = "#" * round(size / total_size * 20)
102
+ out += f"{_fmt_size(size):>10} {percent:>3.0f}% {entry:<20} {bar}\n"
103
+
104
+ if below:
105
+ other_size = sum(size for _, size, _ in below)
106
+ other_percent = (other_size / total_size) * 100
107
+ other_count = len(below)
108
+ bar = "#" * round(other_size / total_size * 20)
109
+ out += f"{_fmt_size(other_size):>10} {other_percent:>3.0f}% {f'<other {other_count}>':<20} {bar}\n"
110
+
111
+ return out