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
@@ -0,0 +1,316 @@
1
+ """
2
+ Step: ORM Optimization Analysis
3
+ Analyze ORM usage and provide optimization suggestions.
4
+ """
5
+
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Dict, List, Set, Tuple, Optional
9
+
10
+ from .base import Step, StepResult
11
+
12
+
13
+ class ORMOptimizationStep(Step):
14
+ """Analyze ORM usage patterns and suggest optimizations."""
15
+
16
+ name = "orm optimization"
17
+
18
+ # Django-specific patterns
19
+ DJANGO_PATTERNS = {
20
+ "bulk_create": r"\.bulk_create\(",
21
+ "bulk_update": r"\.bulk_update\(",
22
+ "select_related": r"\.select_related\(",
23
+ "prefetch_related": r"\.prefetch_related\(",
24
+ "only": r"\.only\(",
25
+ "defer": r"\.defer\(",
26
+ "values": r"\.values\(",
27
+ "values_list": r"\.values_list\(",
28
+ "exists": r"\.exists\(",
29
+ "count": r"\.count\(",
30
+ }
31
+
32
+ # SQLAlchemy patterns
33
+ SQLALCHEMY_PATTERNS = {
34
+ "joinedload": r"joinedload\(",
35
+ "selectinload": r"selectinload\(",
36
+ "contains_eager": r"contains_eager\(",
37
+ "defer": r"defer\(",
38
+ "load_only": r"load_only\(",
39
+ "raiseload": r"raiseload\(",
40
+ }
41
+
42
+ def run(self, ctx: "BundleContext") -> StepResult: # type: ignore[name-defined]
43
+ """Analyze ORM optimization patterns."""
44
+ import time
45
+
46
+ start = time.time()
47
+
48
+ root = ctx.root
49
+
50
+ # Analyze ORM usage
51
+ analysis = self._analyze_orm_optimization(root)
52
+
53
+ # Generate report
54
+ lines = [
55
+ "=" * 80,
56
+ "ORM OPTIMIZATION ANALYSIS REPORT",
57
+ "=" * 80,
58
+ "",
59
+ ]
60
+
61
+ # Summary
62
+ lines.extend(
63
+ [
64
+ "SUMMARY",
65
+ "=" * 80,
66
+ "",
67
+ f"Primary ORM: {analysis['primary_orm'] or 'None detected'}",
68
+ "",
69
+ ]
70
+ )
71
+
72
+ if not analysis["primary_orm"]:
73
+ lines.extend(
74
+ [
75
+ "⊘ No ORM detected",
76
+ "",
77
+ "This project does not appear to use a common ORM.",
78
+ "If this is incorrect, ensure ORM imports are visible.",
79
+ "",
80
+ ]
81
+ )
82
+ else:
83
+ # Django Analysis
84
+ if analysis["primary_orm"] == "Django":
85
+ lines.extend(
86
+ [
87
+ "DJANGO ORM OPTIMIZATION",
88
+ "=" * 80,
89
+ "",
90
+ ]
91
+ )
92
+
93
+ django_analysis = analysis["django"]
94
+
95
+ # Optimization techniques used
96
+ lines.append("Optimization Techniques Used:")
97
+ lines.append("")
98
+
99
+ for technique, count in sorted(django_analysis.items()):
100
+ if count > 0:
101
+ lines.append(f" ✓ {technique}: {count} usage(s)")
102
+
103
+ if not any(count > 0 for count in django_analysis.values()):
104
+ lines.append(" ⚠ No optimization techniques detected")
105
+
106
+ lines.append("")
107
+
108
+ # Missing optimizations
109
+ lines.extend(
110
+ [
111
+ "OPTIMIZATION OPPORTUNITIES",
112
+ "-" * 80,
113
+ "",
114
+ ]
115
+ )
116
+
117
+ if django_analysis.get("select_related", 0) == 0:
118
+ lines.append(
119
+ " • Missing select_related():"
120
+ )
121
+ lines.append(
122
+ " Useful for ForeignKey and OneToOneField relationships"
123
+ )
124
+ lines.append(" Reduces queries by joining related tables")
125
+ lines.append("")
126
+
127
+ if django_analysis.get("prefetch_related", 0) == 0:
128
+ lines.append(
129
+ " • Missing prefetch_related():"
130
+ )
131
+ lines.append(
132
+ " Useful for ManyToManyField and reverse ForeignKey"
133
+ )
134
+ lines.append(
135
+ " Fetches related objects in separate optimized queries"
136
+ )
137
+ lines.append("")
138
+
139
+ if django_analysis.get("only", 0) == 0 and django_analysis.get(
140
+ "defer", 0
141
+ ) == 0:
142
+ lines.append(" • Consider using only() or defer():")
143
+ lines.append(" Reduces data transferred from database")
144
+ lines.append(" Useful for large text/blob fields")
145
+ lines.append("")
146
+
147
+ if django_analysis.get("bulk_create", 0) == 0:
148
+ lines.append(" • Missing bulk operations:")
149
+ lines.append(" Use bulk_create() for batch inserts")
150
+ lines.append(" Use bulk_update() for batch updates")
151
+ lines.append("")
152
+
153
+ if django_analysis.get("exists", 0) == 0:
154
+ lines.append(" • Consider using exists() for existence checks:")
155
+ lines.append(" Faster than count() when just checking presence")
156
+ lines.append("")
157
+
158
+ # SQLAlchemy Analysis
159
+ elif analysis["primary_orm"] == "SQLAlchemy":
160
+ lines.extend(
161
+ [
162
+ "SQLALCHEMY ORM OPTIMIZATION",
163
+ "=" * 80,
164
+ "",
165
+ ]
166
+ )
167
+
168
+ sqlalchemy_analysis = analysis["sqlalchemy"]
169
+
170
+ # Optimization techniques used
171
+ lines.append("Eager Loading Techniques Used:")
172
+ lines.append("")
173
+
174
+ for technique, count in sorted(sqlalchemy_analysis.items()):
175
+ if count > 0:
176
+ lines.append(f" ✓ {technique}: {count} usage(s)")
177
+
178
+ if not any(count > 0 for count in sqlalchemy_analysis.values()):
179
+ lines.append(" ⚠ No eager loading techniques detected")
180
+
181
+ lines.append("")
182
+
183
+ # Recommendations
184
+ lines.extend(
185
+ [
186
+ "OPTIMIZATION OPPORTUNITIES",
187
+ "-" * 80,
188
+ "",
189
+ ]
190
+ )
191
+
192
+ if sqlalchemy_analysis.get("joinedload", 0) == 0:
193
+ lines.append(" • Missing joinedload():")
194
+ lines.append(" Performs eager loading with SQL joins")
195
+ lines.append(" Reduces number of queries")
196
+ lines.append("")
197
+
198
+ if sqlalchemy_analysis.get("selectinload", 0) == 0:
199
+ lines.append(" • Missing selectinload():")
200
+ lines.append(" Performs eager loading with IN clause")
201
+ lines.append(" Better for large collections")
202
+ lines.append("")
203
+
204
+ if sqlalchemy_analysis.get("raiseload", 0) == 0:
205
+ lines.append(" • Consider using raiseload():")
206
+ lines.append(" Catches lazy-loaded accesses (helpful for debugging)")
207
+ lines.append("")
208
+
209
+ # Tortoise ORM
210
+ elif analysis["primary_orm"] == "Tortoise ORM":
211
+ lines.extend(
212
+ [
213
+ "TORTOISE ORM OPTIMIZATION",
214
+ "=" * 80,
215
+ "",
216
+ " • Use prefetch_related() for relationships",
217
+ " • Use select_related() for ForeignKey",
218
+ " • Consider indexed fields for frequently filtered columns",
219
+ " • Use bulk_create() for multiple insertions",
220
+ "",
221
+ ]
222
+ )
223
+
224
+ # General recommendations
225
+ lines.extend(
226
+ [
227
+ "=" * 80,
228
+ "GENERAL OPTIMIZATION PRINCIPLES",
229
+ "=" * 80,
230
+ "",
231
+ "1. USE QUERY ANALYSIS TOOLS",
232
+ " - Django Debug Toolbar: Inspect query count and time",
233
+ " - SQLAlchemy echo=True: Log all queries",
234
+ " - Database EXPLAIN: Analyze query execution plans",
235
+ "",
236
+ "2. OPTIMIZE COMMON PATTERNS",
237
+ " - Batch operations (bulk_create, bulk_update)",
238
+ " - Eager loading (select_related, prefetch_related)",
239
+ " - Field selection (only, defer, values, values_list)",
240
+ "",
241
+ "3. DATABASE LEVEL",
242
+ " - Add indexes to filtered/joined columns",
243
+ " - Denormalize if query patterns justify it",
244
+ " - Use database-level aggregations when possible",
245
+ "",
246
+ "4. CACHING",
247
+ " - Cache frequently accessed objects",
248
+ " - Use query result caching",
249
+ " - Consider materialized views for complex aggregations",
250
+ "",
251
+ "5. MONITORING",
252
+ " - Track slow queries in production",
253
+ " - Monitor database connection pool",
254
+ " - Alert on query performance degradation",
255
+ "",
256
+ ]
257
+ )
258
+
259
+ # Write report
260
+ output = "\n".join(lines)
261
+ dest = ctx.workdir / "logs" / "141_orm_optimization.txt"
262
+ dest.parent.mkdir(parents=True, exist_ok=True)
263
+ dest.write_text(output, encoding="utf-8")
264
+
265
+ elapsed = int(time.time() - start)
266
+ return StepResult(self.name, "OK", elapsed, "")
267
+
268
+ def _analyze_orm_optimization(self, root: Path) -> Dict:
269
+ """Analyze ORM optimization patterns."""
270
+ primary_orm = None
271
+ django_patterns = {k: 0 for k in self.DJANGO_PATTERNS}
272
+ sqlalchemy_patterns = {k: 0 for k in self.SQLALCHEMY_PATTERNS}
273
+
274
+ python_files = list(root.rglob("*.py"))
275
+
276
+ for py_file in python_files:
277
+ if any(
278
+ part in py_file.parts
279
+ for part in ["venv", ".venv", "env", "__pycache__", "site-packages"]
280
+ ):
281
+ continue
282
+
283
+ try:
284
+ source = py_file.read_text(encoding="utf-8", errors="ignore")
285
+
286
+ # Detect ORM
287
+ if "from django.db" in source:
288
+ if not primary_orm:
289
+ primary_orm = "Django"
290
+ # Count Django patterns
291
+ for pattern_name, pattern in self.DJANGO_PATTERNS.items():
292
+ django_patterns[pattern_name] += len(
293
+ re.findall(pattern, source)
294
+ )
295
+
296
+ elif "from sqlalchemy" in source:
297
+ if not primary_orm:
298
+ primary_orm = "SQLAlchemy"
299
+ # Count SQLAlchemy patterns
300
+ for pattern_name, pattern in self.SQLALCHEMY_PATTERNS.items():
301
+ sqlalchemy_patterns[pattern_name] += len(
302
+ re.findall(pattern, source)
303
+ )
304
+
305
+ elif "from tortoise" in source:
306
+ if not primary_orm:
307
+ primary_orm = "Tortoise ORM"
308
+
309
+ except (OSError, UnicodeDecodeError):
310
+ continue
311
+
312
+ return {
313
+ "primary_orm": primary_orm,
314
+ "django": django_patterns,
315
+ "sqlalchemy": sqlalchemy_patterns,
316
+ }
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess # nosec B404 - Required for tool execution, paths validated
4
+ import time
5
+ from dataclasses import dataclass
6
+
7
+ from .base import StepResult
8
+ from ..context import BundleContext
9
+ from ..tools import which
10
+
11
+
12
+ @dataclass
13
+ class PipAuditStep:
14
+ name: str = "pip-audit"
15
+ outfile: str = "logs/51_pip_audit.txt"
16
+
17
+ def run(self, ctx: BundleContext) -> StepResult:
18
+ start = time.time()
19
+ out = ctx.workdir / self.outfile
20
+ out.parent.mkdir(parents=True, exist_ok=True)
21
+
22
+ pip_audit = which("pip-audit")
23
+ if not pip_audit:
24
+ out.write_text(
25
+ "pip-audit not found; skipping (pip install pip-audit)\n",
26
+ encoding="utf-8",
27
+ )
28
+ return StepResult(self.name, "SKIP", 0, "missing pip-audit")
29
+
30
+ # Run pip-audit to check for known vulnerabilities
31
+ cmd = [pip_audit, "--desc", "--format", "columns"]
32
+ header = f"## PWD: {ctx.root}\n## CMD: {' '.join(cmd)}\n\n"
33
+
34
+ cp = subprocess.run( # nosec B603
35
+ cmd, cwd=str(ctx.root), text=True, capture_output=True, check=False
36
+ )
37
+ text = header + (cp.stdout or "") + ("\n" + cp.stderr if cp.stderr else "")
38
+ out.write_text(ctx.redact_text(text), encoding="utf-8")
39
+
40
+ dur = int(time.time() - start)
41
+ # pip-audit exit codes: 0=no vulnerabilities, 1=vulnerabilities found
42
+ note = (
43
+ "" if cp.returncode == 0 else f"exit={cp.returncode} (vulnerable packages)"
44
+ )
45
+ return StepResult(self.name, "PASS", dur, note)
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess # nosec B404 - Required for tool execution, paths validated
4
+ import time
5
+ from dataclasses import dataclass
6
+
7
+ from .base import StepResult
8
+ from ..context import BundleContext
9
+ from ..tools import which
10
+
11
+
12
+ @dataclass
13
+ class PipdeptreeStep:
14
+ name: str = "pipdeptree"
15
+ outfile: str = "meta/30_dependency_tree.txt"
16
+
17
+ def run(self, ctx: BundleContext) -> StepResult:
18
+ start = time.time()
19
+ out = ctx.workdir / self.outfile
20
+ out.parent.mkdir(parents=True, exist_ok=True)
21
+
22
+ pipdeptree = which("pipdeptree")
23
+ if not pipdeptree:
24
+ out.write_text(
25
+ "pipdeptree not found; skipping (pip install pipdeptree)\n",
26
+ encoding="utf-8",
27
+ )
28
+ return StepResult(self.name, "SKIP", 0, "missing pipdeptree")
29
+
30
+ # Run pipdeptree with warnings for conflicts
31
+ cmd = [
32
+ pipdeptree,
33
+ "--warn",
34
+ "fail", # Show warnings for conflicting dependencies
35
+ ]
36
+
37
+ try:
38
+ result = subprocess.run( # nosec B603 - Using full path from which()
39
+ cmd,
40
+ cwd=ctx.root,
41
+ stdout=subprocess.PIPE,
42
+ stderr=subprocess.PIPE,
43
+ text=True,
44
+ timeout=60,
45
+ )
46
+
47
+ # Combine stdout and stderr to capture both tree and warnings
48
+ output = result.stdout
49
+ if result.stderr:
50
+ output += "\n\n=== WARNINGS ===\n" + result.stderr
51
+
52
+ out.write_text(output, encoding="utf-8")
53
+ elapsed = int((time.time() - start) * 1000)
54
+
55
+ # pipdeptree returns 0 on success, even with warnings
56
+ return StepResult(self.name, "OK", elapsed, "")
57
+ except subprocess.TimeoutExpired:
58
+ out.write_text("pipdeptree timed out after 60s\n", encoding="utf-8")
59
+ return StepResult(self.name, "FAIL", 60000, "timeout")
60
+ except Exception as e:
61
+ out.write_text(f"pipdeptree error: {e}\n", encoding="utf-8")
62
+ return StepResult(self.name, "FAIL", 0, str(e))