gwc-pybundle 2.1.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.

Potentially problematic release.


This version of gwc-pybundle might be problematic. Click here for more details.

Files changed (82) hide show
  1. gwc_pybundle-2.1.2.dist-info/METADATA +903 -0
  2. gwc_pybundle-2.1.2.dist-info/RECORD +82 -0
  3. gwc_pybundle-2.1.2.dist-info/WHEEL +5 -0
  4. gwc_pybundle-2.1.2.dist-info/entry_points.txt +2 -0
  5. gwc_pybundle-2.1.2.dist-info/licenses/LICENSE.md +25 -0
  6. gwc_pybundle-2.1.2.dist-info/top_level.txt +1 -0
  7. pybundle/__init__.py +0 -0
  8. pybundle/__main__.py +4 -0
  9. pybundle/cli.py +546 -0
  10. pybundle/context.py +404 -0
  11. pybundle/doctor.py +148 -0
  12. pybundle/filters.py +228 -0
  13. pybundle/manifest.py +77 -0
  14. pybundle/packaging.py +45 -0
  15. pybundle/policy.py +132 -0
  16. pybundle/profiles.py +454 -0
  17. pybundle/roadmap_model.py +42 -0
  18. pybundle/roadmap_scan.py +328 -0
  19. pybundle/root_detect.py +14 -0
  20. pybundle/runner.py +180 -0
  21. pybundle/steps/__init__.py +26 -0
  22. pybundle/steps/ai_context.py +791 -0
  23. pybundle/steps/api_docs.py +219 -0
  24. pybundle/steps/asyncio_analysis.py +358 -0
  25. pybundle/steps/bandit.py +72 -0
  26. pybundle/steps/base.py +20 -0
  27. pybundle/steps/blocking_call_detection.py +291 -0
  28. pybundle/steps/call_graph.py +219 -0
  29. pybundle/steps/compileall.py +76 -0
  30. pybundle/steps/config_docs.py +319 -0
  31. pybundle/steps/config_validation.py +302 -0
  32. pybundle/steps/container_image.py +294 -0
  33. pybundle/steps/context_expand.py +272 -0
  34. pybundle/steps/copy_pack.py +293 -0
  35. pybundle/steps/coverage.py +101 -0
  36. pybundle/steps/cprofile_step.py +166 -0
  37. pybundle/steps/dependency_sizes.py +136 -0
  38. pybundle/steps/django_checks.py +214 -0
  39. pybundle/steps/dockerfile_lint.py +282 -0
  40. pybundle/steps/dockerignore.py +311 -0
  41. pybundle/steps/duplication.py +103 -0
  42. pybundle/steps/env_completeness.py +269 -0
  43. pybundle/steps/env_var_usage.py +253 -0
  44. pybundle/steps/error_refs.py +204 -0
  45. pybundle/steps/event_loop_patterns.py +280 -0
  46. pybundle/steps/exception_patterns.py +190 -0
  47. pybundle/steps/fastapi_integration.py +250 -0
  48. pybundle/steps/flask_debugging.py +312 -0
  49. pybundle/steps/git_analytics.py +315 -0
  50. pybundle/steps/handoff_md.py +176 -0
  51. pybundle/steps/import_time.py +175 -0
  52. pybundle/steps/interrogate.py +106 -0
  53. pybundle/steps/license_scan.py +96 -0
  54. pybundle/steps/line_profiler.py +117 -0
  55. pybundle/steps/link_validation.py +287 -0
  56. pybundle/steps/logging_analysis.py +233 -0
  57. pybundle/steps/memory_profile.py +176 -0
  58. pybundle/steps/migration_history.py +336 -0
  59. pybundle/steps/mutation_testing.py +141 -0
  60. pybundle/steps/mypy.py +103 -0
  61. pybundle/steps/orm_optimization.py +316 -0
  62. pybundle/steps/pip_audit.py +45 -0
  63. pybundle/steps/pipdeptree.py +62 -0
  64. pybundle/steps/pylance.py +562 -0
  65. pybundle/steps/pytest.py +66 -0
  66. pybundle/steps/query_pattern_analysis.py +334 -0
  67. pybundle/steps/radon.py +161 -0
  68. pybundle/steps/repro_md.py +161 -0
  69. pybundle/steps/rg_scans.py +78 -0
  70. pybundle/steps/roadmap.py +153 -0
  71. pybundle/steps/ruff.py +117 -0
  72. pybundle/steps/secrets_detection.py +235 -0
  73. pybundle/steps/security_headers.py +309 -0
  74. pybundle/steps/shell.py +74 -0
  75. pybundle/steps/slow_tests.py +178 -0
  76. pybundle/steps/sqlalchemy_validation.py +269 -0
  77. pybundle/steps/test_flakiness.py +184 -0
  78. pybundle/steps/tree.py +116 -0
  79. pybundle/steps/type_coverage.py +277 -0
  80. pybundle/steps/unused_deps.py +211 -0
  81. pybundle/steps/vulture.py +167 -0
  82. pybundle/tools.py +63 -0
pybundle/filters.py ADDED
@@ -0,0 +1,228 @@
1
+ from fnmatch import fnmatch
2
+ from pathlib import Path
3
+
4
+ EXCLUDE_PATTERNS = {
5
+ "*.egg",
6
+ "*.egg-info",
7
+ "*.appimage",
8
+ "*.deb",
9
+ "*.rpm",
10
+ "*.exe",
11
+ "*.msi",
12
+ "*.dmg",
13
+ "*.pkg",
14
+ "*.so",
15
+ "*.dll",
16
+ "*.dylib",
17
+ "*.db",
18
+ "*.sqlite",
19
+ "*.sqlite3",
20
+ "*.zip",
21
+ "*.tar",
22
+ "*.gz",
23
+ "*.tgz",
24
+ "*.bz2",
25
+ "*.xz",
26
+ "*.7z",
27
+ "*.rej",
28
+ "*.orig",
29
+ }
30
+
31
+ DEFAULT_EXCLUDE_DIRS = {
32
+ ".git",
33
+ ".venv",
34
+ ".mypy_cache",
35
+ ".ruff_cache",
36
+ ".pytest_cache",
37
+ "__pycache__",
38
+ "node_modules",
39
+ "dist",
40
+ "build",
41
+ "target",
42
+ ".next",
43
+ ".nuxt",
44
+ "artifacts",
45
+ ".cache",
46
+ ".hg",
47
+ ".svn",
48
+ "venv",
49
+ "env", # Added
50
+ ".direnv",
51
+ ".pybundle-venv",
52
+ ".freeze-venv", # Added - common in enterprise freeze workflows
53
+ "site-packages", # Added - never scan installed packages
54
+ "binaries",
55
+ "out",
56
+ ".svelte-kit",
57
+ }
58
+
59
+ DEFAULT_INCLUDE_FILES = [
60
+ "pyproject.toml",
61
+ "requirements.txt",
62
+ "poetry.lock",
63
+ "pdm.lock",
64
+ "uv.lock",
65
+ "setup.cfg",
66
+ "setup.py",
67
+ "mypy.ini",
68
+ "ruff.toml",
69
+ ".ruff.toml",
70
+ "pytest.ini",
71
+ "tox.ini",
72
+ ".python-version",
73
+ "README.md",
74
+ "README.rst",
75
+ "README.txt",
76
+ "CHANGELOG.md",
77
+ "LICENSE",
78
+ "LICENSE.md",
79
+ ".tox",
80
+ ".nox",
81
+ ".direnv",
82
+ "requirements-dev.txt",
83
+ "package.json",
84
+ "package-lock.json",
85
+ "pnpm-lock.yaml",
86
+ "yarn.lock",
87
+ "tsconfig.json",
88
+ "vite.config.js",
89
+ "vite.config.ts",
90
+ "webpack.config.js",
91
+ "webpack.config.ts",
92
+ "Cargo.toml",
93
+ "Cargo.lock",
94
+ "tauri.conf.json",
95
+ "tauri.conf.json5",
96
+ "tauri.conf.toml",
97
+ ]
98
+
99
+ DEFAULT_INCLUDE_DIRS = [
100
+ "src",
101
+ "tests",
102
+ "tools",
103
+ "docs",
104
+ ".github",
105
+ "app",
106
+ "templates",
107
+ "static",
108
+ "src-tauri",
109
+ "frontend",
110
+ "web",
111
+ "ui",
112
+ ]
113
+
114
+ DEFAULT_INCLUDE_GLOBS = [
115
+ "*.py",
116
+ "*/**/*.py",
117
+ "templates/**/*",
118
+ "static/**/*",
119
+ ]
120
+
121
+ DEFAULT_EXCLUDE_FILE_EXTS: set[str] = {
122
+ ".appimage",
123
+ ".deb",
124
+ ".rpm",
125
+ ".exe",
126
+ ".msi",
127
+ ".dmg",
128
+ ".pkg",
129
+ ".so",
130
+ ".dll",
131
+ ".dylib",
132
+ ".db",
133
+ ".sqlite",
134
+ ".sqlite3",
135
+ ".zip",
136
+ ".tar",
137
+ ".gz",
138
+ ".tgz",
139
+ ".bz2",
140
+ ".xz",
141
+ ".7z",
142
+ ".git",
143
+ ".venv",
144
+ ".mypy_cache",
145
+ ".ruff_cache",
146
+ ".pytest_cache",
147
+ "__pycache__",
148
+ "node_modules",
149
+ "dist",
150
+ "build",
151
+ "artifacts",
152
+ ".cache",
153
+ }
154
+
155
+
156
+ def is_excluded_by_name(
157
+ name: str, *, exclude_names: set[str], exclude_patterns: set[str]
158
+ ) -> bool:
159
+ if name in exclude_names:
160
+ return True
161
+ return any(fnmatch(name, pat) for pat in exclude_patterns)
162
+
163
+
164
+ def is_excluded_name(self, name: str) -> bool:
165
+ return is_excluded_by_name(
166
+ name, exclude_names=self.exclude_dirs, exclude_patterns=self.exclude_patterns
167
+ )
168
+
169
+
170
+ def is_excluded_path(
171
+ rel: Path,
172
+ exclude_names: set[str],
173
+ exclude_patterns: set[str],
174
+ ) -> bool:
175
+ # Exclude if *any* part matches (dirs) OR the final filename matches
176
+ for part in rel.parts:
177
+ if is_excluded_by_name(
178
+ part, exclude_names=exclude_names, exclude_patterns=exclude_patterns
179
+ ):
180
+ return True
181
+ return False
182
+
183
+
184
+ def should_exclude_from_analysis(path: Path) -> bool:
185
+ """Check if path should be excluded from project analysis.
186
+
187
+ This is a comprehensive filter for ALL analysis steps to prevent
188
+ scanning dependencies, caches, and build artifacts.
189
+
190
+ Use this for: type coverage, docstring coverage, link validation,
191
+ complexity analysis, and any other metrics that should reflect
192
+ PROJECT code only, not dependencies.
193
+ """
194
+ parts = set(path.parts)
195
+
196
+ # Comprehensive list of dependency/cache/build directories
197
+ exclude_patterns = {
198
+ # Virtual environments (all common patterns)
199
+ ".venv", "venv", "env", ".env",
200
+ ".pybundle-venv", ".freeze-venv",
201
+ # Match any venv-like directory (e.g., .gaslog-venv)
202
+ # Check if any part ends with -venv or starts with venv
203
+ }
204
+
205
+ # Additional checks for venv-like patterns
206
+ for part in parts:
207
+ if part.endswith("-venv") or part.endswith("_venv"):
208
+ return True
209
+ if "site-packages" in part:
210
+ return True
211
+
212
+ # Standard exclusions
213
+ if parts & {
214
+ # Caches
215
+ ".mypy_cache", ".ruff_cache", ".pytest_cache", "__pycache__",
216
+ ".cache", ".tox", ".nox",
217
+ # Build outputs
218
+ "build", "dist", "target", "out", "artifacts",
219
+ # Version control
220
+ ".git", ".hg", ".svn",
221
+ # Dependencies
222
+ "node_modules",
223
+ # Other
224
+ "binaries",
225
+ }:
226
+ return True
227
+
228
+ return False
pybundle/manifest.py ADDED
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess # nosec B404 - Required for git operations, paths validated
5
+ from dataclasses import asdict
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .context import BundleContext
10
+
11
+
12
+ def _git_commit_hash(root: Path, git_path: str | None) -> str | None:
13
+ """Return HEAD commit hash if root is inside a git repo, else None."""
14
+ if not git_path:
15
+ return None
16
+ try:
17
+ # If this fails, we're not in a repo or git isn't functional.
18
+ p = subprocess.run( # nosec B603
19
+ [git_path, "rev-parse", "HEAD"],
20
+ cwd=str(root),
21
+ capture_output=True,
22
+ text=True,
23
+ check=True,
24
+ )
25
+ return p.stdout.strip() or None
26
+ except Exception:
27
+ return None
28
+
29
+
30
+ def write_manifest(
31
+ *,
32
+ ctx: BundleContext,
33
+ profile_name: str,
34
+ archive_path: Path,
35
+ archive_format_used: str,
36
+ ) -> None:
37
+ """Write a stable, machine-readable manifest for automation."""
38
+ git_hash = _git_commit_hash(ctx.root, ctx.tools.git)
39
+
40
+ manifest: dict[str, Any] = {
41
+ "schema_version": 1,
42
+ "tool": {"name": "pybundle"},
43
+ "timestamp_utc": ctx.ts,
44
+ "profile": profile_name,
45
+ "paths": {
46
+ "root": str(ctx.root),
47
+ "workdir": str(ctx.workdir),
48
+ "srcdir": str(ctx.srcdir),
49
+ "logdir": str(ctx.logdir),
50
+ "metadir": str(ctx.metadir),
51
+ },
52
+ "outputs": {
53
+ "archive": {
54
+ "path": str(archive_path),
55
+ "name": archive_path.name,
56
+ "format": archive_format_used,
57
+ },
58
+ "summary_json": str(ctx.summary_json),
59
+ "manifest_json": str(ctx.manifest_json),
60
+ "runlog": str(ctx.runlog),
61
+ },
62
+ "options": asdict(ctx.options),
63
+ "run": {
64
+ "strict": ctx.strict,
65
+ "redact": ctx.redact,
66
+ "keep_workdir": ctx.keep_workdir,
67
+ "archive_format_requested": ctx.archive_format,
68
+ "name_prefix": ctx.name_prefix,
69
+ },
70
+ "tools": asdict(ctx.tools),
71
+ "git": {"commit": git_hash},
72
+ }
73
+
74
+ ctx.manifest_json.write_text(
75
+ json.dumps(manifest, indent=2, sort_keys=True),
76
+ encoding="utf-8",
77
+ )
pybundle/packaging.py ADDED
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess # nosec B404 - Required for archive creation, paths validated
4
+ from pathlib import Path
5
+
6
+ from .context import BundleContext
7
+
8
+
9
+ def resolve_archive_format(ctx: BundleContext) -> str:
10
+ fmt_used = ctx.archive_format
11
+ if fmt_used == "auto":
12
+ fmt_used = "zip" if ctx.tools.zip else "tar.gz"
13
+ return fmt_used
14
+
15
+
16
+ def archive_output_path(ctx: BundleContext, fmt_used: str) -> Path:
17
+ if fmt_used == "zip":
18
+ return ctx.outdir / f"{ctx.name_prefix}.zip"
19
+ return ctx.outdir / f"{ctx.name_prefix}.tar.gz"
20
+
21
+
22
+ def make_archive(ctx: BundleContext) -> tuple[Path, str]:
23
+ fmt_used = resolve_archive_format(ctx)
24
+
25
+ if fmt_used == "zip":
26
+ if not ctx.tools.zip:
27
+ raise RuntimeError("zip tool not available")
28
+ out = archive_output_path(ctx, fmt_used)
29
+ # zip wants working dir above target folder
30
+ subprocess.run( # nosec B603
31
+ [ctx.tools.zip, "-qr", str(out), ctx.workdir.name],
32
+ cwd=str(ctx.workdir.parent),
33
+ check=False,
34
+ )
35
+ return out, fmt_used
36
+
37
+ if not ctx.tools.tar:
38
+ raise RuntimeError("tar tool not available")
39
+ out = archive_output_path(ctx, fmt_used)
40
+ subprocess.run( # nosec B603
41
+ [ctx.tools.tar, "-czf", str(out), ctx.workdir.name],
42
+ cwd=str(ctx.workdir.parent),
43
+ check=False,
44
+ )
45
+ return out, fmt_used
pybundle/policy.py ADDED
@@ -0,0 +1,132 @@
1
+ # pybundle/policy.py
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+
7
+ from pybundle.filters import (
8
+ DEFAULT_EXCLUDE_DIRS,
9
+ DEFAULT_EXCLUDE_FILE_EXTS,
10
+ DEFAULT_INCLUDE_FILES,
11
+ DEFAULT_INCLUDE_DIRS,
12
+ DEFAULT_INCLUDE_GLOBS,
13
+ EXCLUDE_PATTERNS,
14
+ is_excluded_by_name,
15
+ )
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class AIContextPolicy:
20
+ exclude_dirs: set[str] = field(default_factory=lambda: set(DEFAULT_EXCLUDE_DIRS))
21
+ exclude_patterns: set[str] = field(default_factory=lambda: set(EXCLUDE_PATTERNS))
22
+ exclude_file_exts: set[str] = field(
23
+ default_factory=lambda: set(DEFAULT_EXCLUDE_FILE_EXTS)
24
+ )
25
+
26
+ include_files: list[str] = field(
27
+ default_factory=lambda: list(DEFAULT_INCLUDE_FILES)
28
+ )
29
+ include_dirs: list[str] = field(default_factory=lambda: list(DEFAULT_INCLUDE_DIRS))
30
+ include_globs: list[str] = field(
31
+ default_factory=lambda: list(DEFAULT_INCLUDE_GLOBS)
32
+ )
33
+
34
+ # AI-friendly knobs
35
+ tree_max_depth: int = 4
36
+ largest_limit: int = 80
37
+ roadmap_max_files: int = 20000
38
+ roadmap_mermaid_depth: int = 2
39
+ roadmap_mermaid_max_edges: int = 180
40
+
41
+ def include_dir_candidates(self, root: Path) -> list[Path]:
42
+ out: list[Path] = []
43
+ for d in self.include_dirs:
44
+ p = root / d
45
+ if p.exists():
46
+ out.append(p)
47
+ return out or [root]
48
+
49
+
50
+ @dataclass
51
+ class PathFilter:
52
+ """
53
+ Shared filtering logic across steps:
54
+ - prune excluded dir names
55
+ - prune venvs by structure (any name)
56
+ - optionally exclude noisy file types by extension
57
+ """
58
+
59
+ exclude_dirs: set[str]
60
+ exclude_file_exts: set[str]
61
+ exclude_patterns: set[str] = field(default_factory=set)
62
+ detect_venvs: bool = True
63
+
64
+ def is_venv_root(self, p: Path) -> bool:
65
+ if not p.is_dir():
66
+ return False
67
+
68
+ if (p / "pyvenv.cfg").is_file():
69
+ return True
70
+
71
+ if (p / "bin").is_dir():
72
+ if (p / "bin" / "activate").is_file() and (
73
+ (p / "bin" / "python").exists() or (p / "bin" / "python3").exists()
74
+ ):
75
+ return True
76
+ if any((p / "lib").glob("python*/site-packages")):
77
+ return True
78
+
79
+ if (p / "Scripts").is_dir():
80
+ if (p / "Scripts" / "activate").is_file() and (
81
+ (p / "Scripts" / "python.exe").is_file()
82
+ or (p / "Scripts" / "python").exists()
83
+ ):
84
+ return True
85
+ if (p / "Lib" / "site-packages").is_dir():
86
+ return True
87
+
88
+ if (p / ".Python").exists():
89
+ return True
90
+
91
+ return False
92
+
93
+ def should_prune_dir(self, parent_dir: Path, child_name: str) -> bool:
94
+ if is_excluded_by_name(
95
+ child_name,
96
+ exclude_names=self.exclude_dirs,
97
+ exclude_patterns=self.exclude_patterns,
98
+ ):
99
+ return True
100
+ if self.detect_venvs and self.is_venv_root(parent_dir / child_name):
101
+ return True
102
+ return False
103
+
104
+ def should_include_file(self, root: Path, p: Path) -> bool:
105
+ try:
106
+ rel = p.relative_to(root)
107
+ except Exception:
108
+ return False
109
+
110
+ # reject files under excluded dirs by name/pattern
111
+ for part in rel.parts[:-1]:
112
+ if is_excluded_by_name(
113
+ part,
114
+ exclude_names=self.exclude_dirs,
115
+ exclude_patterns=self.exclude_patterns,
116
+ ):
117
+ return False
118
+
119
+ # reject excluded file names by pattern (e.g. *.egg, *.rej)
120
+ if is_excluded_by_name(
121
+ rel.name,
122
+ exclude_names=self.exclude_dirs,
123
+ exclude_patterns=self.exclude_patterns,
124
+ ):
125
+ return False
126
+
127
+ # reject excluded extensions
128
+ ext = p.suffix.lower()
129
+ if ext in self.exclude_file_exts:
130
+ return False
131
+
132
+ return True