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,250 @@
1
+ """
2
+ Step: FastAPI Integration
3
+ Validate FastAPI OpenAPI schema and endpoint documentation.
4
+ """
5
+
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Dict, List, Any, Optional
9
+
10
+ from .base import Step, StepResult
11
+
12
+
13
+ class FastAPIIntegrationStep(Step):
14
+ """Validate FastAPI schema and endpoint completeness."""
15
+
16
+ name = "fastapi schema"
17
+
18
+ def run(self, ctx: "BundleContext") -> StepResult: # type: ignore[name-defined]
19
+ """Validate FastAPI integration."""
20
+ import time
21
+
22
+ start = time.time()
23
+
24
+ root = ctx.root
25
+
26
+ # Find FastAPI app
27
+ app_info = self._find_fastapi_app(root)
28
+ if not app_info:
29
+ elapsed = int(time.time() - start)
30
+ return StepResult(
31
+ self.name, "SKIP", elapsed, "No FastAPI application found"
32
+ )
33
+
34
+ # Analyze endpoints
35
+ endpoints = self._analyze_endpoints(root, app_info)
36
+
37
+ # Generate report
38
+ lines = [
39
+ "=" * 80,
40
+ "FASTAPI INTEGRATION REPORT",
41
+ "=" * 80,
42
+ "",
43
+ ]
44
+
45
+ lines.extend(
46
+ [
47
+ "SUMMARY",
48
+ "=" * 80,
49
+ f"FastAPI app location: {app_info['file']}:{app_info['name']}",
50
+ f"Total endpoints: {len(endpoints)}",
51
+ "",
52
+ ]
53
+ )
54
+
55
+ # Endpoint analysis
56
+ lines.extend(
57
+ [
58
+ "ENDPOINTS",
59
+ "=" * 80,
60
+ "",
61
+ ]
62
+ )
63
+
64
+ documented = 0
65
+ undocumented = 0
66
+
67
+ for endpoint in sorted(endpoints, key=lambda e: e["path"]):
68
+ doc_status = "✓" if endpoint["has_docstring"] else "⚠"
69
+ documented += 1 if endpoint["has_docstring"] else 0
70
+ undocumented += 0 if endpoint["has_docstring"] else 1
71
+
72
+ lines.append(f"{doc_status} {endpoint['method']:6} {endpoint['path']}")
73
+ if endpoint["has_docstring"]:
74
+ lines.append(f" Summary: {endpoint['docstring_first_line']}")
75
+ else:
76
+ lines.append(" ⚠ Missing docstring (won't appear in OpenAPI)")
77
+
78
+ lines.append("")
79
+
80
+ # Statistics
81
+ lines.extend(
82
+ [
83
+ "DOCUMENTATION STATISTICS",
84
+ "-" * 80,
85
+ f"Documented endpoints: {documented}/{len(endpoints)} ({100*documented//len(endpoints)}%)" if endpoints else "No endpoints found",
86
+ f"Undocumented endpoints: {undocumented}/{len(endpoints)}" if endpoints else "",
87
+ "",
88
+ ]
89
+ )
90
+
91
+ # Response models
92
+ lines.extend(
93
+ [
94
+ "RESPONSE MODELS",
95
+ "=" * 80,
96
+ "",
97
+ ]
98
+ )
99
+
100
+ models_count = 0
101
+ for endpoint in endpoints:
102
+ if endpoint.get("response_model"):
103
+ models_count += 1
104
+ lines.append(f" {endpoint['method']:6} {endpoint['path']}")
105
+ lines.append(f" Response: {endpoint['response_model']}")
106
+
107
+ if models_count == 0:
108
+ lines.append(" ℹ No response models found")
109
+
110
+ lines.append("")
111
+
112
+ # Recommendations
113
+ lines.extend(
114
+ [
115
+ "=" * 80,
116
+ "BEST PRACTICES & RECOMMENDATIONS",
117
+ "=" * 80,
118
+ "",
119
+ "1. DOCUMENTATION",
120
+ " ✓ Add docstrings to all endpoint handlers",
121
+ " ✓ Use triple-quoted docstrings for OpenAPI summary",
122
+ " ✓ Include examples in docstrings for clarity",
123
+ "",
124
+ "2. RESPONSE MODELS",
125
+ " ✓ Define Pydantic models for all responses",
126
+ " ✓ Use response_model parameter in @app.get/post/etc",
127
+ " ✓ Include status_code for non-200 responses",
128
+ "",
129
+ "3. REQUEST VALIDATION",
130
+ " ✓ Use Pydantic models for request bodies",
131
+ " ✓ Use Path, Query, Header for parameter validation",
132
+ " ✓ Provide examples in Field descriptions",
133
+ "",
134
+ "4. ERROR HANDLING",
135
+ " ✓ Define responses for 400, 404, 500, etc",
136
+ " ✓ Use HTTPException with detail messages",
137
+ " ✓ Document possible error responses in docstrings",
138
+ "",
139
+ "5. SECURITY",
140
+ " ✓ Use security=[] parameter for protected endpoints",
141
+ " ✓ Document security requirements in OpenAPI",
142
+ " ✓ Validate bearer tokens and API keys",
143
+ "",
144
+ "6. TESTING",
145
+ " ✓ Use TestClient for endpoint testing",
146
+ " ✓ Test all response status codes",
147
+ " ✓ Test request validation (happy path + errors)",
148
+ "",
149
+ ]
150
+ )
151
+
152
+ # Write report
153
+ output = "\n".join(lines)
154
+ dest = ctx.workdir / "logs" / "150_fastapi_schema.txt"
155
+ dest.parent.mkdir(parents=True, exist_ok=True)
156
+ dest.write_text(output, encoding="utf-8")
157
+
158
+ elapsed = int(time.time() - start)
159
+ return StepResult(self.name, "OK", elapsed, "")
160
+
161
+ def _find_fastapi_app(self, root: Path) -> Optional[Dict[str, Any]]:
162
+ """Find FastAPI app instance."""
163
+ python_files = list(root.rglob("*.py"))
164
+
165
+ for py_file in python_files:
166
+ if any(
167
+ part in py_file.parts
168
+ for part in ["venv", ".venv", "env", "__pycache__", "site-packages"]
169
+ ):
170
+ continue
171
+
172
+ try:
173
+ source = py_file.read_text(encoding="utf-8", errors="ignore")
174
+
175
+ # Look for FastAPI imports and app creation
176
+ if "from fastapi import" in source or "import fastapi" in source:
177
+ if "app = FastAPI" in source or "app: FastAPI" in source:
178
+ # Extract app variable name
179
+ import re
180
+
181
+ match = re.search(r"(\w+)\s*=\s*FastAPI\(", source)
182
+ if match:
183
+ return {
184
+ "file": str(py_file.relative_to(root)),
185
+ "name": match.group(1),
186
+ }
187
+ except (OSError, UnicodeDecodeError):
188
+ continue
189
+
190
+ return None
191
+
192
+ def _analyze_endpoints(self, root: Path, app_info: Dict) -> List[Dict[str, Any]]:
193
+ """Analyze FastAPI endpoints."""
194
+ endpoints = []
195
+
196
+ # Look for route decorators in the app file
197
+ app_file = root / app_info["file"]
198
+ if not app_file.exists():
199
+ return endpoints
200
+
201
+ try:
202
+ source = app_file.read_text(encoding="utf-8", errors="ignore")
203
+
204
+ # Look for decorators: @app.get, @app.post, etc.
205
+ import re
206
+
207
+ # Pattern: @app.method("path")
208
+ pattern = r"@(?:app|router)\.(get|post|put|delete|patch|head|options)\(['\"]([^'\"]+)['\"]"
209
+ for match in re.finditer(pattern, source):
210
+ method, path = match.groups()
211
+ endpoints.append(
212
+ {
213
+ "method": method.upper(),
214
+ "path": path,
215
+ "has_docstring": False,
216
+ "docstring_first_line": "",
217
+ "response_model": None,
218
+ }
219
+ )
220
+
221
+ # Check for docstrings in functions following decorators
222
+ lines = source.split("\n")
223
+ for i, line in enumerate(lines):
224
+ if re.search(r"@(?:app|router)\.(get|post|put|delete|patch|head|options)", line):
225
+ # Look for function definition
226
+ for j in range(i + 1, min(i + 5, len(lines))):
227
+ if "def " in lines[j]:
228
+ # Check for docstring
229
+ if j + 1 < len(lines):
230
+ next_line = lines[j + 1].strip()
231
+ if next_line.startswith('"""') or next_line.startswith("'''"):
232
+ docstring = next_line.replace('"""', "").replace("'''", "").strip()
233
+ if endpoints:
234
+ endpoints[-1]["has_docstring"] = True
235
+ endpoints[-1]["docstring_first_line"] = docstring
236
+ break
237
+
238
+ # Check for response_model
239
+ for i, line in enumerate(lines):
240
+ if "response_model=" in line and endpoints:
241
+ import re
242
+
243
+ match = re.search(r"response_model=(\w+)", line)
244
+ if match:
245
+ endpoints[-1]["response_model"] = match.group(1)
246
+
247
+ except Exception:
248
+ pass
249
+
250
+ return endpoints
@@ -0,0 +1,312 @@
1
+ """
2
+ Step: Flask Debugging
3
+ Detect Flask debug mode and other security issues in Flask apps.
4
+ """
5
+
6
+ import os
7
+ import re
8
+ from pathlib import Path
9
+ from typing import Dict, List, Tuple, Optional
10
+
11
+ from .base import Step, StepResult
12
+
13
+
14
+ class FlaskDebuggingStep(Step):
15
+ """Detect Flask debug mode and security issues."""
16
+
17
+ name = "flask debugging"
18
+
19
+ def run(self, ctx: "BundleContext") -> StepResult: # type: ignore[name-defined]
20
+ """Check Flask debugging configuration."""
21
+ import time
22
+
23
+ start = time.time()
24
+
25
+ root = ctx.root
26
+
27
+ # Find Flask app
28
+ flask_info = self._find_flask_app(root)
29
+ if not flask_info:
30
+ elapsed = int(time.time() - start)
31
+ return StepResult(
32
+ self.name, "SKIP", elapsed, "No Flask application found"
33
+ )
34
+
35
+ # Analyze Flask configuration
36
+ config_issues = self._analyze_flask_config(root, flask_info)
37
+
38
+ # Generate report
39
+ lines = [
40
+ "=" * 80,
41
+ "FLASK SECURITY & DEBUGGING REPORT",
42
+ "=" * 80,
43
+ "",
44
+ ]
45
+
46
+ lines.extend(
47
+ [
48
+ "SUMMARY",
49
+ "=" * 80,
50
+ f"Flask app location: {flask_info['file']}:{flask_info['name']}",
51
+ "",
52
+ ]
53
+ )
54
+
55
+ # Configuration issues
56
+ lines.extend(
57
+ [
58
+ "SECURITY ISSUES",
59
+ "=" * 80,
60
+ "",
61
+ ]
62
+ )
63
+
64
+ if config_issues:
65
+ for level, issue, detail in config_issues:
66
+ icon = "✗" if level == "ERROR" else "⚠"
67
+ lines.append(f"{icon} [{level}] {issue}")
68
+ if detail:
69
+ lines.append(f" {detail}")
70
+ lines.append("")
71
+ else:
72
+ lines.append("✓ No critical security issues detected")
73
+ lines.append("")
74
+
75
+ # Debug mode analysis
76
+ lines.extend(
77
+ [
78
+ "DEBUG MODE ANALYSIS",
79
+ "-" * 80,
80
+ "",
81
+ ]
82
+ )
83
+
84
+ debug_findings = self._check_debug_mode(root, flask_info)
85
+ for finding in debug_findings:
86
+ lines.append(f" • {finding}")
87
+
88
+ lines.append("")
89
+
90
+ # Routes analysis
91
+ lines.extend(
92
+ [
93
+ "ROUTES ANALYSIS",
94
+ "=" * 80,
95
+ "",
96
+ ]
97
+ )
98
+
99
+ routes = self._extract_routes(root, flask_info)
100
+ if routes:
101
+ lines.append(f"Found {len(routes)} route(s):")
102
+ lines.append("")
103
+ for method, path, handler in routes:
104
+ lines.append(f" {method:6} {path:40} -> {handler}")
105
+ else:
106
+ lines.append(" ℹ No routes found via static analysis")
107
+
108
+ lines.append("")
109
+
110
+ # Recommendations
111
+ lines.extend(
112
+ [
113
+ "=" * 80,
114
+ "FLASK DEPLOYMENT BEST PRACTICES",
115
+ "=" * 80,
116
+ "",
117
+ "1. DEBUG MODE",
118
+ " ✗ NEVER use DEBUG=True in production",
119
+ " ✓ Use environment variables to control debug mode",
120
+ " ✓ Set DEBUG=False in production configuration",
121
+ " ✓ Use app.config.from_object('config.Production')",
122
+ "",
123
+ "2. SECRET KEY",
124
+ " ✓ Use strong, random SECRET_KEY (os.urandom(24))",
125
+ " ✓ Store SECRET_KEY in environment variables",
126
+ " ✓ Never hardcode secrets in source code",
127
+ " ✓ Rotate SECRET_KEY regularly in production",
128
+ "",
129
+ "3. SESSION SECURITY",
130
+ " ✓ Set SESSION_COOKIE_SECURE=True",
131
+ " ✓ Set SESSION_COOKIE_HTTPONLY=True",
132
+ " ✓ Set SESSION_COOKIE_SAMESITE='Lax' or 'Strict'",
133
+ " ✓ Use secure session backends (Redis, database)",
134
+ "",
135
+ "4. CORS & HEADERS",
136
+ " ✓ Use Flask-CORS with restricted origins",
137
+ " ✓ Set X-Frame-Options: DENY",
138
+ " ✓ Set X-Content-Type-Options: nosniff",
139
+ " ✓ Set X-XSS-Protection: 1; mode=block",
140
+ " ✓ Set Strict-Transport-Security (HSTS)",
141
+ "",
142
+ "5. ERROR HANDLING",
143
+ " ✓ Implement custom 404, 500 error handlers",
144
+ " ✓ Log errors securely (no sensitive data)",
145
+ " ✓ Hide stack traces in production",
146
+ " ✓ Return generic error messages to users",
147
+ "",
148
+ "6. DEPENDENCIES",
149
+ " ✓ Use pip-audit to check for vulnerabilities",
150
+ " ✓ Keep Flask and extensions updated",
151
+ " ✓ Review security advisories regularly",
152
+ "",
153
+ "7. DEPLOYMENT",
154
+ " ✓ Use WSGI server (Gunicorn, uWSGI, Waitress)",
155
+ " ✓ Run behind reverse proxy (Nginx, Apache)",
156
+ " ✓ Use HTTPS with valid certificate",
157
+ " ✓ Enable security headers in reverse proxy",
158
+ "",
159
+ ]
160
+ )
161
+
162
+ # Write report
163
+ output = "\n".join(lines)
164
+ dest = ctx.workdir / "logs" / "151_flask_checks.txt"
165
+ dest.parent.mkdir(parents=True, exist_ok=True)
166
+ dest.write_text(output, encoding="utf-8")
167
+
168
+ elapsed = int(time.time() - start)
169
+ return StepResult(self.name, "OK", elapsed, "")
170
+
171
+ def _find_flask_app(self, root: Path) -> Optional[Dict[str, str]]:
172
+ """Find Flask app instance."""
173
+ python_files = list(root.rglob("*.py"))
174
+
175
+ for py_file in python_files:
176
+ if any(
177
+ part in py_file.parts
178
+ for part in ["venv", ".venv", "env", "__pycache__", "site-packages"]
179
+ ):
180
+ continue
181
+
182
+ try:
183
+ source = py_file.read_text(encoding="utf-8", errors="ignore")
184
+
185
+ # Look for Flask imports and app creation
186
+ if "from flask import" in source or "import flask" in source:
187
+ if "Flask(" in source:
188
+ # Extract app variable name
189
+ match = re.search(r"(\w+)\s*=\s*Flask\(", source)
190
+ if match:
191
+ return {
192
+ "file": str(py_file.relative_to(root)),
193
+ "name": match.group(1),
194
+ }
195
+ except (OSError, UnicodeDecodeError):
196
+ continue
197
+
198
+ return None
199
+
200
+ def _analyze_flask_config(
201
+ self, root: Path, flask_info: Dict[str, str]
202
+ ) -> List[Tuple[str, str, str]]:
203
+ """Analyze Flask configuration for security issues."""
204
+ issues = []
205
+
206
+ # Check for hardcoded secrets
207
+ python_files = list(root.rglob("*.py"))
208
+ for py_file in python_files:
209
+ if any(
210
+ part in py_file.parts
211
+ for part in ["venv", ".venv", "env", "__pycache__", "site-packages"]
212
+ ):
213
+ continue
214
+
215
+ try:
216
+ source = py_file.read_text(encoding="utf-8", errors="ignore")
217
+
218
+ # Check for hardcoded SECRET_KEY
219
+ if re.search(
220
+ r"['\"]SECRET_KEY['\"]?\s*[:=]\s*['\"](?!<|{{)", source
221
+ ):
222
+ issues.append(
223
+ (
224
+ "ERROR",
225
+ "Hardcoded SECRET_KEY detected",
226
+ f"File: {py_file.relative_to(root)}",
227
+ )
228
+ )
229
+
230
+ # Check for DEBUG=True in code
231
+ if re.search(r"DEBUG\s*=\s*True", source):
232
+ issues.append(
233
+ (
234
+ "ERROR",
235
+ "DEBUG=True found in code",
236
+ f"File: {py_file.relative_to(root)} - Use environment variables instead",
237
+ )
238
+ )
239
+
240
+ except (OSError, UnicodeDecodeError):
241
+ continue
242
+
243
+ return issues
244
+
245
+ def _check_debug_mode(self, root: Path, flask_info: Dict[str, str]) -> List[str]:
246
+ """Check debug mode configuration."""
247
+ findings = []
248
+
249
+ app_file = root / flask_info["file"]
250
+ if app_file.exists():
251
+ try:
252
+ source = app_file.read_text(encoding="utf-8", errors="ignore")
253
+
254
+ if "app.run(debug=True)" in source:
255
+ findings.append(
256
+ "⚠ app.run(debug=True) found - ensure this is only in development"
257
+ )
258
+
259
+ if "FLASK_ENV" in source:
260
+ findings.append(
261
+ "✓ FLASK_ENV environment variable is used"
262
+ )
263
+
264
+ if "os.getenv('DEBUG')" in source or "os.environ.get('DEBUG')" in source:
265
+ findings.append(
266
+ "✓ DEBUG mode controlled via environment variable"
267
+ )
268
+
269
+ if not any(
270
+ pattern in source
271
+ for pattern in ["debug=", "FLASK_ENV", "DEBUG", "app.run()"]
272
+ ):
273
+ findings.append(
274
+ "ℹ No explicit debug configuration found - verify Flask configuration chain"
275
+ )
276
+
277
+ except (OSError, UnicodeDecodeError):
278
+ pass
279
+
280
+ return findings
281
+
282
+ def _extract_routes(self, root: Path, flask_info: Dict[str, str]) -> List[Tuple[str, str, str]]:
283
+ """Extract Flask routes."""
284
+ routes = []
285
+
286
+ app_file = root / flask_info["file"]
287
+ if not app_file.exists():
288
+ return routes
289
+
290
+ try:
291
+ source = app_file.read_text(encoding="utf-8", errors="ignore")
292
+ lines = source.split("\n")
293
+
294
+ # Pattern: @app.route or @app.get, @app.post, etc.
295
+ for i, line in enumerate(lines):
296
+ route_match = re.search(r"@(?:app|blueprint)\.(route|get|post|put|delete)\(['\"]([^'\"]+)", line)
297
+ if route_match:
298
+ method = route_match.group(1).upper()
299
+ path = route_match.group(2)
300
+
301
+ # Find handler function
302
+ for j in range(i + 1, min(i + 3, len(lines))):
303
+ func_match = re.search(r"def\s+(\w+)\s*\(", lines[j])
304
+ if func_match:
305
+ handler = func_match.group(1)
306
+ routes.append((method, path, handler))
307
+ break
308
+
309
+ except Exception:
310
+ pass
311
+
312
+ return routes