gwc-pybundle 0.4.2__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.
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ import time
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from .base import StepResult
9
+ from ..context import BundleContext
10
+
11
+
12
+ @dataclass
13
+ class ShellStep:
14
+ name: str
15
+ outfile_rel: str
16
+ cmd: list[str]
17
+ cwd_is_root: bool = True
18
+ allow_fail: bool = True
19
+ require_cmd: str | None = None
20
+
21
+ @property
22
+ def out_rel(self) -> str:
23
+ return self.outfile_rel
24
+
25
+ def run(self, ctx: BundleContext) -> StepResult:
26
+ if self.require_cmd and not getattr(ctx.tools, self.require_cmd, None):
27
+ out = ctx.workdir / self.outfile_rel
28
+ out.parent.mkdir(parents=True, exist_ok=True)
29
+ out.write_text(
30
+ f"{self.require_cmd} not found; skipping\n", encoding="utf-8"
31
+ )
32
+ return StepResult(self.name, "SKIP", 0, f"missing {self.require_cmd}")
33
+
34
+ out = ctx.workdir / self.outfile_rel
35
+ out.parent.mkdir(parents=True, exist_ok=True)
36
+
37
+ start = time.time()
38
+ header = (
39
+ f"## PWD: {ctx.root if self.cwd_is_root else Path.cwd()}\n"
40
+ f"## CMD: {' '.join(self.cmd)}\n\n"
41
+ )
42
+
43
+ try:
44
+ cp = subprocess.run(
45
+ self.cmd,
46
+ cwd=str(ctx.root) if self.cwd_is_root else None,
47
+ text=True,
48
+ capture_output=True,
49
+ check=False,
50
+ )
51
+ text = header + (cp.stdout or "") + ("\n" + cp.stderr if cp.stderr else "")
52
+ out.write_text(ctx.redact_text(text), encoding="utf-8")
53
+ status = (
54
+ "PASS"
55
+ if cp.returncode == 0
56
+ else ("FAIL" if not self.allow_fail else "PASS")
57
+ )
58
+ note = "" if cp.returncode == 0 else f"exit={cp.returncode}"
59
+ except Exception as e:
60
+ out.write_text(
61
+ ctx.redact_text(header + f"\nEXCEPTION: {e}\n"), encoding="utf-8"
62
+ )
63
+ status = "FAIL" if not self.allow_fail else "PASS"
64
+ note = str(e)
65
+
66
+ dur = int(time.time() - start)
67
+ return StepResult(self.name, status, dur, note)
pybundle/steps/tree.py ADDED
@@ -0,0 +1,136 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import time
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+
8
+ from .base import StepResult
9
+ from pybundle.context import BundleContext
10
+ from pybundle.tools import which
11
+ from pybundle.policy import AIContextPolicy, PathFilter
12
+
13
+ BIN_EXTS = {
14
+ ".appimage", ".deb", ".rpm", ".exe", ".msi", ".dmg", ".pkg",
15
+ ".so", ".dll", ".dylib",
16
+ }
17
+ DB_EXTS = {".db", ".sqlite", ".sqlite3"}
18
+ ARCHIVE_EXTS = {".zip", ".tar", ".gz", ".tgz", ".bz2", ".xz", ".7z"}
19
+
20
+ DEFAULT_EXCLUDES = [
21
+ ".git",
22
+ ".venv",
23
+ ".mypy_cache",
24
+ ".ruff_cache",
25
+ ".pytest_cache",
26
+ "__pycache__",
27
+ "node_modules",
28
+ "dist",
29
+ "build",
30
+ "artifacts",
31
+ ".cache",
32
+ ]
33
+
34
+ def _is_excluded(rel: Path, excludes: set[str]) -> bool:
35
+ # Exclude if any path component matches
36
+ for part in rel.parts:
37
+ if part in excludes:
38
+ return True
39
+ return False
40
+
41
+ @dataclass
42
+ class TreeStep:
43
+ name: str = "tree (filtered)"
44
+ max_depth: int = 4
45
+ excludes: list[str] | None = None
46
+ policy: AIContextPolicy | None = None
47
+
48
+ def run(self, ctx: BundleContext) -> StepResult:
49
+ start = time.time()
50
+ policy = self.policy or AIContextPolicy()
51
+
52
+ # allow overrides
53
+ exclude_dirs = set(self.excludes) if self.excludes else set(policy.exclude_dirs)
54
+ filt = PathFilter(exclude_dirs=exclude_dirs, exclude_file_exts=set(policy.exclude_file_exts))
55
+
56
+ out = ctx.metadir / "10_tree.txt"
57
+ out.parent.mkdir(parents=True, exist_ok=True)
58
+
59
+ root = ctx.root
60
+ lines: list[str] = []
61
+
62
+ for dirpath, dirnames, filenames in os.walk(root):
63
+ dp = Path(dirpath)
64
+ rel_dp = dp.relative_to(root)
65
+ depth = 0 if rel_dp == Path(".") else len(rel_dp.parts)
66
+
67
+ if depth > self.max_depth:
68
+ dirnames[:] = []
69
+ continue
70
+
71
+ # prune dirs (name + venv-structure)
72
+ kept = []
73
+ for d in dirnames:
74
+ if filt.should_prune_dir(dp, d):
75
+ continue
76
+ kept.append(d)
77
+ dirnames[:] = kept
78
+
79
+ for fn in filenames:
80
+ p = dp / fn
81
+ if not filt.should_include_file(root, p):
82
+ continue
83
+ lines.append(str(p.relative_to(root)))
84
+
85
+ lines.sort()
86
+ out.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")
87
+ dur = int(time.time() - start)
88
+ return StepResult(self.name, "PASS", dur, "python-walk")
89
+
90
+ @dataclass
91
+ class LargestFilesStep:
92
+ name: str = "largest files"
93
+ limit: int = 80
94
+ excludes: list[str] | None = None
95
+ policy: AIContextPolicy | None = None
96
+
97
+ def run(self, ctx: BundleContext) -> StepResult:
98
+ start = time.time()
99
+ policy = self.policy or AIContextPolicy()
100
+
101
+ exclude_dirs = set(self.excludes) if self.excludes else set(policy.exclude_dirs)
102
+ filt = PathFilter(exclude_dirs=exclude_dirs, exclude_file_exts=set(policy.exclude_file_exts))
103
+
104
+ out = ctx.metadir / "11_largest_files.txt"
105
+ out.parent.mkdir(parents=True, exist_ok=True)
106
+
107
+ files: list[tuple[int, str]] = []
108
+ root = ctx.root
109
+
110
+ for dirpath, dirnames, filenames in os.walk(root):
111
+ dp = Path(dirpath)
112
+
113
+ kept = []
114
+ for d in dirnames:
115
+ if filt.should_prune_dir(dp, d):
116
+ continue
117
+ kept.append(d)
118
+ dirnames[:] = kept
119
+
120
+ for fn in filenames:
121
+ p = dp / fn
122
+ if not filt.should_include_file(root, p):
123
+ continue
124
+ try:
125
+ size = p.stat().st_size
126
+ except OSError:
127
+ continue
128
+ files.append((size, str(p.relative_to(root))))
129
+
130
+ files.sort(key=lambda x: x[0], reverse=True)
131
+ lines = [f"{size}\t{path}" for size, path in files[: self.limit]]
132
+ out.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")
133
+
134
+ dur = int(time.time() - start)
135
+ return StepResult(self.name, "PASS", dur, f"count={len(files)}")
136
+
pybundle/tools.py ADDED
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+
5
+
6
+ def which(cmd: str) -> str | None:
7
+ return shutil.which(cmd)