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,791 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess # nosec B404 - Required for tool execution, paths validated
5
+ import time
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from .base import StepResult
10
+ from ..context import BundleContext
11
+ from ..filters import should_exclude_from_analysis
12
+
13
+
14
+ @dataclass
15
+ class AIContextStep:
16
+ """Generate AI_CONTEXT.md file with auto-detected project information."""
17
+
18
+ name: str = "AI context"
19
+ outfile: str = "AI_CONTEXT.md"
20
+
21
+ def run(self, ctx: BundleContext) -> StepResult:
22
+ start = time.time()
23
+ out = ctx.workdir / self.outfile
24
+ out.parent.mkdir(parents=True, exist_ok=True)
25
+
26
+ try:
27
+ content = self._generate_ai_context(ctx)
28
+ out.write_text(content, encoding="utf-8")
29
+
30
+ elapsed = int((time.time() - start) * 1000)
31
+ return StepResult(self.name, "OK", elapsed, "")
32
+ except Exception as e:
33
+ out.write_text(f"Error generating AI_CONTEXT.md: {e}\n", encoding="utf-8")
34
+ return StepResult(
35
+ self.name, "FAIL", int((time.time() - start) * 1000), str(e)
36
+ )
37
+
38
+ def _generate_ai_context(self, ctx: BundleContext) -> str:
39
+ """Generate the AI_CONTEXT.md content with auto-detected information."""
40
+ sections = []
41
+
42
+ # Header
43
+ sections.append(self._generate_header(ctx))
44
+
45
+ # 0) What this project is
46
+ sections.append(self._generate_project_overview(ctx))
47
+
48
+ # 1) How to run it
49
+ sections.append(self._generate_how_to_run(ctx))
50
+
51
+ # 2) Repository map
52
+ sections.append(self._generate_repo_map(ctx))
53
+
54
+ # 3) Architecture & patterns
55
+ sections.append(self._generate_architecture(ctx))
56
+
57
+ # 4) Configuration
58
+ sections.append(self._generate_configuration(ctx))
59
+
60
+ # 5) Data model & storage
61
+ sections.append(self._generate_data_model(ctx))
62
+
63
+ # 6) API surface
64
+ sections.append(self._generate_api_surface(ctx))
65
+
66
+ # 7) Tests & quality gates
67
+ sections.append(self._generate_tests_quality(ctx))
68
+
69
+ # 8) Sharp edges
70
+ sections.append(self._generate_sharp_edges(ctx))
71
+
72
+ # 9) Change guide
73
+ sections.append(self._generate_change_guide(ctx))
74
+
75
+ # 10) AI rules
76
+ sections.append(self._generate_ai_rules(ctx))
77
+
78
+ return "\n\n".join(sections)
79
+
80
+ def _generate_header(self, ctx: BundleContext) -> str:
81
+ """Generate the header section."""
82
+ project_name = ctx.root.name
83
+ return f"""# AI_CONTEXT.md
84
+
85
+ > **Auto-generated by pybundle** — {time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime())}
86
+ >
87
+ > **Purpose**: Give an AI assistant (or a new dev) just enough truth to make safe, consistent changes without guessing.
88
+ >
89
+ > **Rules**: Prefer facts over vibes. Link to files. Keep it updated.
90
+
91
+ **Project**: `{project_name}`
92
+
93
+ ---
94
+
95
+ **How to interpret this document:**
96
+
97
+ * **Known Facts** — Extracted directly from code/configs with file:line refs (high confidence)
98
+ * **Inferred** — Pattern matching auto-detection (verify before trusting)
99
+
100
+ When you see file paths or specific references, treat as known fact. When you see "(detected)" without refs, verify first."""
101
+
102
+ def _generate_project_overview(self, ctx: BundleContext) -> str:
103
+ """Generate section 0: What this project is."""
104
+ # Try to extract from README
105
+ readme_path = ctx.root / "README.md"
106
+ description = "Python project"
107
+
108
+ if readme_path.exists():
109
+ try:
110
+ content = readme_path.read_text(encoding="utf-8", errors="ignore")
111
+ lines = content.splitlines()
112
+ # Look for first non-empty, non-heading line as description
113
+ for line in lines[1:10]: # Check first 10 lines
114
+ line = line.strip()
115
+ if line and not line.startswith("#") and not line.startswith("!"):
116
+ description = line
117
+ break
118
+ except Exception:
119
+ pass
120
+
121
+ # Detect framework
122
+ framework = self._detect_framework(ctx.root)
123
+ framework_note = f" (using {framework})" if framework else ""
124
+
125
+ return f"""---
126
+
127
+ ## 0) What this project is
128
+
129
+ **One-liner:** {description}
130
+
131
+ **Type:** {framework_note if framework_note else "Python application"}
132
+
133
+ **Core functionality:**
134
+ {self._detect_core_functionality(ctx.root)}
135
+
136
+ **Entry points:**
137
+ {self._list_entrypoints(ctx.root)}"""
138
+
139
+ def _generate_how_to_run(self, ctx: BundleContext) -> str:
140
+ """Generate section 1: How to run it."""
141
+ python_version = "3.9+"
142
+ if ctx.tools.python:
143
+ try:
144
+ result = subprocess.run( # nosec B603
145
+ [ctx.tools.python, "--version"],
146
+ capture_output=True,
147
+ text=True,
148
+ timeout=5,
149
+ )
150
+ if result.returncode == 0:
151
+ python_version = result.stdout.strip().replace("Python ", "")
152
+ except Exception:
153
+ pass
154
+
155
+ # Detect requirements file
156
+ req_file = "requirements.txt"
157
+ if (ctx.root / "pyproject.toml").exists():
158
+ req_file = "pyproject.toml (using pip install .)"
159
+
160
+ # Detect how to run
161
+ run_command = self._detect_run_command(ctx.root)
162
+
163
+ return f"""---
164
+
165
+ ## 1) How to run it (local)
166
+
167
+ **Prerequisites:**
168
+
169
+ * Python: `{python_version}`
170
+ * Dependencies: See `{req_file}`
171
+
172
+ **Install:**
173
+
174
+ ```bash
175
+ python -m venv .venv
176
+ source .venv/bin/activate # Windows: .venv\\Scripts\\activate
177
+ pip install -r requirements.txt
178
+ ```
179
+
180
+ **Run:**
181
+
182
+ ```bash
183
+ {run_command}
184
+ ```
185
+
186
+ **Development tasks:**
187
+
188
+ ```bash
189
+ # Format code
190
+ ruff format .
191
+
192
+ # Lint
193
+ ruff check .
194
+
195
+ # Type check
196
+ mypy .
197
+
198
+ # Run tests
199
+ pytest
200
+ ```"""
201
+
202
+ def _generate_repo_map(self, ctx: BundleContext) -> str:
203
+ """Generate section 2: Repository map."""
204
+ # Find key directories
205
+ key_dirs = []
206
+ for dir_name in ["app", "src", "lib", "core", "api", "routers", "models"]:
207
+ dir_path = ctx.root / dir_name
208
+ if dir_path.is_dir():
209
+ key_dirs.append(f"* `{dir_name}/` — {self._describe_directory(dir_path)}")
210
+
211
+ # Find key files
212
+ key_files = []
213
+ for file_name in ["main.py", "app.py", "__main__.py", "server.py", "cli.py"]:
214
+ file_path = ctx.root / file_name
215
+ if file_path.exists():
216
+ key_files.append(f"* `{file_name}` — Main entry point")
217
+
218
+ dirs_section = "\n".join(key_dirs) if key_dirs else "* (Auto-detection found no standard structure)"
219
+ files_section = "\n".join(key_files) if key_files else "* (No main entry files detected)"
220
+
221
+ return f"""---
222
+
223
+ ## 2) Repository map (what to read first)
224
+
225
+ ### Primary entry points
226
+
227
+ {files_section}
228
+
229
+ ### Key directories
230
+
231
+ {dirs_section}
232
+
233
+ ### Tests
234
+
235
+ * `tests/` — Test suite (if present)"""
236
+
237
+ def _generate_architecture(self, ctx: BundleContext) -> str:
238
+ """Generate section 3: Architecture & patterns."""
239
+ framework = self._detect_framework(ctx.root)
240
+
241
+ patterns = []
242
+ if self._file_exists_in_tree(ctx.root, "*router*.py"):
243
+ patterns.append("* Routing-based architecture (routers define endpoints)")
244
+ if self._file_exists_in_tree(ctx.root, "*service*.py"):
245
+ patterns.append("* Service layer pattern (business logic in services)")
246
+ if self._file_exists_in_tree(ctx.root, "*repository*.py") or self._file_exists_in_tree(ctx.root, "*repo.py"):
247
+ patterns.append("* Repository pattern (data access abstraction)")
248
+ if self._file_exists_in_tree(ctx.root, "middleware*.py"):
249
+ patterns.append("* Middleware for cross-cutting concerns")
250
+
251
+ patterns_section = "\n".join(patterns) if patterns else "* (No clear architectural patterns auto-detected)"
252
+
253
+ return f"""---
254
+
255
+ ## 3) Architecture & patterns
256
+
257
+ **Framework:** {framework or "Not detected"}
258
+
259
+ **Key patterns:**
260
+
261
+ {patterns_section}
262
+
263
+ **Error handling:**
264
+
265
+ {self._detect_error_handling(ctx.root)}
266
+
267
+ **Logging:**
268
+
269
+ {self._detect_logging(ctx.root)}"""
270
+
271
+ def _generate_configuration(self, ctx: BundleContext) -> str:
272
+ """Generate section 4: Configuration."""
273
+ env_vars = self._extract_env_vars(ctx.root)
274
+
275
+ required = []
276
+ optional = []
277
+
278
+ for var in sorted(env_vars.keys()):
279
+ refs = env_vars[var]
280
+ # Show first 3 file:line references
281
+ ref_str = ", ".join(f"{path}:{line}" for path, line in refs[:3])
282
+ if len(refs) > 3:
283
+ ref_str += f" (+{len(refs) - 3} more)"
284
+
285
+ # Heuristic: vars with "SECRET", "KEY", "TOKEN" are likely required
286
+ if any(x in var.upper() for x in ["SECRET", "KEY", "TOKEN", "PASSWORD"]):
287
+ required.append(f"* `{var}` — used in [{ref_str}]")
288
+ else:
289
+ optional.append(f"* `{var}` — used in [{ref_str}]")
290
+
291
+ required_section = "\n".join(required) if required else "* (None detected)"
292
+ optional_section = "\n".join(optional) if optional else "* (None detected)"
293
+
294
+ config_files = []
295
+ for cf in ["pyproject.toml", "setup.cfg", "mypy.ini", ".env.example"]:
296
+ if (ctx.root / cf).exists():
297
+ config_files.append(f"* `{cf}`")
298
+
299
+ config_section = "\n".join(config_files) if config_files else "* (None detected)"
300
+
301
+ return f"""---
302
+
303
+ ## 4) Configuration (env vars + config files)
304
+
305
+ **Required environment variables:**
306
+
307
+ {required_section}
308
+
309
+ **Optional environment variables:**
310
+
311
+ {optional_section}
312
+
313
+ **Config files:**
314
+
315
+ {config_section}"""
316
+
317
+ def _generate_data_model(self, ctx: BundleContext) -> str:
318
+ """Generate section 5: Data model & storage."""
319
+ # Detect database usage
320
+ db_type = self._detect_database(ctx.root)
321
+ orm = self._detect_orm(ctx.root)
322
+
323
+ # Find model files
324
+ model_files = []
325
+ for pattern in ["*model*.py", "*schema*.py"]:
326
+ for p in ctx.root.rglob(pattern):
327
+ if not any(x in p.parts for x in [".venv", "venv", "__pycache__", "artifacts"]):
328
+ rel_path = p.relative_to(ctx.root)
329
+ model_files.append(f"* `{rel_path}` — Data models/schemas")
330
+ if len(model_files) >= 3: # Limit to 3 files
331
+ break
332
+
333
+ models_section = "\n".join(model_files) if model_files else "* (No model files detected)"
334
+
335
+ return f"""---
336
+
337
+ ## 5) Data model & storage
338
+
339
+ **Database:** {db_type or "Not detected"}
340
+
341
+ **ORM/Data layer:** {orm or "Not detected"}
342
+
343
+ **Model files:**
344
+
345
+ {models_section}
346
+
347
+ **Migrations:**
348
+
349
+ {self._detect_migrations(ctx.root)}"""
350
+
351
+ def _generate_api_surface(self, ctx: BundleContext) -> str:
352
+ """Generate section 6: API surface."""
353
+ # Detect API routes
354
+ routes = self._extract_routes(ctx.root)
355
+
356
+ if routes:
357
+ routes_section = "\n".join(f"* `{r}`" for r in routes[:10]) # Limit to 10
358
+ else:
359
+ routes_section = "* (No API routes auto-detected)"
360
+
361
+ return f"""---
362
+
363
+ ## 6) API surface (what's stable)
364
+
365
+ **Detected endpoints:**
366
+
367
+ {routes_section}
368
+
369
+ **Note:** This is auto-detected. Verify actual endpoints in your router files."""
370
+
371
+ def _generate_tests_quality(self, ctx: BundleContext) -> str:
372
+ """Generate section 7: Tests & quality gates."""
373
+ test_framework = "pytest" if self._file_exists_in_tree(ctx.root, "test_*.py") else "Not detected"
374
+ test_dir = "tests/" if (ctx.root / "tests").is_dir() else "Not detected"
375
+
376
+ return f"""---
377
+
378
+ ## 7) Tests & quality gates
379
+
380
+ **Test framework:** {test_framework}
381
+
382
+ **Test location:** `{test_dir}`
383
+
384
+ **Run tests:**
385
+
386
+ ```bash
387
+ pytest
388
+ ```
389
+
390
+ **Quality checks:**
391
+
392
+ ```bash
393
+ # Linting
394
+ ruff check .
395
+
396
+ # Type checking
397
+ mypy .
398
+
399
+ # Code coverage
400
+ pytest --cov
401
+ ```"""
402
+
403
+ def _generate_sharp_edges(self, ctx: BundleContext) -> str:
404
+ """Generate section 8: Sharp edges."""
405
+ return """---
406
+
407
+ ## 8) Sharp edges (gotchas)
408
+
409
+ **Known issues:**
410
+
411
+ * (Add known issues manually)
412
+
413
+ **Common mistakes:**
414
+
415
+ * (Add common mistakes manually)
416
+
417
+ **Performance notes:**
418
+
419
+ * (Add performance considerations manually)"""
420
+
421
+ def _generate_change_guide(self, ctx: BundleContext) -> str:
422
+ """Generate section 9: Change guide."""
423
+ return """---
424
+
425
+ ## 9) Change guide (common tasks)
426
+
427
+ **Add a new API endpoint:**
428
+
429
+ 1. Create route handler in appropriate router file
430
+ 2. Add validation schemas if needed
431
+ 3. Update tests
432
+ 4. Document in OpenAPI/docstring
433
+
434
+ **Add a new database model:**
435
+
436
+ 1. Define model in models file
437
+ 2. Create migration (if using migrations)
438
+ 3. Update related services/repositories
439
+ 4. Add tests
440
+
441
+ **Add a new dependency:**
442
+
443
+ ```bash
444
+ pip install <package>
445
+ pip freeze > requirements.txt
446
+ ```"""
447
+
448
+ def _generate_ai_rules(self, ctx: BundleContext) -> str:
449
+ """Generate section 10: AI rules."""
450
+ return """---
451
+
452
+ ## 10) AI rules (when changing code)
453
+
454
+ **ALWAYS:**
455
+
456
+ * Preserve existing error handling patterns
457
+ * Maintain type hints on all function signatures
458
+ * Update tests when changing behavior
459
+ * Keep existing logging format
460
+ * Follow the patterns in existing code
461
+
462
+ **NEVER:**
463
+
464
+ * Remove type hints
465
+ * Skip error handling
466
+ * Change API contracts without discussion
467
+ * Remove existing tests
468
+ * Modify configuration defaults without noting it
469
+
470
+ **WHEN IN DOUBT:**
471
+
472
+ * Ask before changing database schemas
473
+ * Ask before adding new dependencies
474
+ * Ask before changing API response shapes
475
+ * Check existing code for similar patterns"""
476
+
477
+ # Helper methods for detection
478
+
479
+ def _detect_framework(self, root: Path) -> str | None:
480
+ """Detect web framework used."""
481
+ for p in root.rglob("*.py"):
482
+ if any(x in p.parts for x in [".venv", "venv", "__pycache__"]):
483
+ continue
484
+ try:
485
+ content = p.read_text(encoding="utf-8", errors="ignore")
486
+ if "from fastapi" in content or "import fastapi" in content:
487
+ return "FastAPI"
488
+ if "from flask" in content or "import flask" in content:
489
+ return "Flask"
490
+ if "from django" in content or "import django" in content:
491
+ return "Django"
492
+ except Exception:
493
+ continue
494
+ return None
495
+
496
+ def _detect_core_functionality(self, root: Path) -> str:
497
+ """Detect core functionality from code structure."""
498
+ features = []
499
+
500
+ if self._file_exists_in_tree(root, "*api*.py") or self._file_exists_in_tree(root, "*router*.py"):
501
+ features.append("* REST API endpoints")
502
+ if self._file_exists_in_tree(root, "*template*.html") or (root / "templates").is_dir():
503
+ features.append("* Server-side HTML rendering")
504
+ if self._file_exists_in_tree(root, "*websocket*.py"):
505
+ features.append("* WebSocket support")
506
+ if self._file_exists_in_tree(root, "*celery*.py") or self._file_exists_in_tree(root, "*task*.py"):
507
+ features.append("* Background task processing")
508
+ if self._file_exists_in_tree(root, "*auth*.py"):
509
+ features.append("* Authentication/authorization")
510
+
511
+ return "\n".join(features) if features else "* (Auto-detection could not determine core features)"
512
+
513
+ def _list_entrypoints(self, root: Path) -> str:
514
+ """List detected entry points."""
515
+ entrypoints = []
516
+
517
+ # Check for __main__.py
518
+ if (root / "__main__.py").exists():
519
+ entrypoints.append("* `python -m <package>` (via __main__.py)")
520
+
521
+ # Check for main app blocks in common locations
522
+ for pattern in ["main.py", "app.py", "server.py", "*/main.py", "*/app.py"]:
523
+ for p in root.glob(pattern):
524
+ if should_exclude_from_analysis(p):
525
+ continue
526
+ try:
527
+ content = p.read_text(encoding="utf-8", errors="ignore")
528
+ if 'if __name__ == "__main__"' in content:
529
+ rel_path = p.relative_to(root)
530
+ entrypoints.append(f"* `python {rel_path}` (__main__ block)")
531
+ except Exception:
532
+ pass
533
+
534
+ # Detect FastAPI/uvicorn apps
535
+ for p in root.rglob("*.py"):
536
+ if should_exclude_from_analysis(p):
537
+ continue
538
+ try:
539
+ content = p.read_text(encoding="utf-8", errors="ignore")
540
+ if "FastAPI(" in content:
541
+ # Extract variable name
542
+ import re
543
+ match = re.search(r'(\w+)\s*=\s*FastAPI\(', content)
544
+ if match:
545
+ app_var = match.group(1)
546
+ rel_path = str(p.relative_to(root)).replace("/", ".").replace("\\", ".").replace(".py", "")
547
+ entrypoints.append(f"* `uvicorn {rel_path}:{app_var}` (FastAPI)")
548
+ except Exception:
549
+ pass
550
+
551
+ # Check for console_scripts in pyproject.toml
552
+ pyproject = root / "pyproject.toml"
553
+ if pyproject.exists():
554
+ try:
555
+ import tomllib
556
+ with open(pyproject, "rb") as f:
557
+ data = tomllib.load(f)
558
+
559
+ scripts = data.get("project", {}).get("scripts", {})
560
+ for name, target in scripts.items():
561
+ entrypoints.append(f"* `{name}` → `{target}` (console script)")
562
+
563
+ poetry_scripts = data.get("tool", {}).get("poetry", {}).get("scripts", {})
564
+ for name, target in poetry_scripts.items():
565
+ entrypoints.append(f"* `{name}` → `{target}` (Poetry script)")
566
+ except Exception:
567
+ # Fallback to text search
568
+ try:
569
+ content = pyproject.read_text(encoding="utf-8")
570
+ if "[project.scripts]" in content or "[tool.poetry.scripts]" in content:
571
+ entrypoints.append("* Console scripts (see pyproject.toml)")
572
+ except Exception:
573
+ pass
574
+
575
+ return "\n".join(entrypoints) if entrypoints else "* (No entry points detected)"
576
+
577
+ def _detect_run_command(self, root: Path) -> str:
578
+ """Detect how to run the application."""
579
+ # Check for FastAPI apps first (most specific)
580
+ for p in root.rglob("*.py"):
581
+ if should_exclude_from_analysis(p):
582
+ continue
583
+ try:
584
+ content = p.read_text(encoding="utf-8", errors="ignore")
585
+ if "FastAPI(" in content:
586
+ import re
587
+ match = re.search(r'(\w+)\s*=\s*FastAPI\(', content)
588
+ if match:
589
+ app_var = match.group(1)
590
+ rel_path = str(p.relative_to(root)).replace("/", ".").replace("\\", ".").replace(".py", "")
591
+ return f"uvicorn {rel_path}:{app_var} --reload"
592
+ except Exception:
593
+ pass
594
+
595
+ # Check for common patterns
596
+ if (root / "main.py").exists():
597
+ return "python main.py"
598
+ if (root / "app.py").exists():
599
+ return "python app.py"
600
+ if (root / "__main__.py").exists():
601
+ return f"python -m {root.name}"
602
+
603
+ return "# Run command not auto-detected, see README.md"
604
+
605
+ def _describe_directory(self, dir_path: Path) -> str:
606
+ """Provide a brief description of a directory's purpose."""
607
+ dir_name = dir_path.name
608
+
609
+ descriptions = {
610
+ "app": "Application code",
611
+ "src": "Source code",
612
+ "api": "API layer",
613
+ "routers": "Route handlers",
614
+ "models": "Data models",
615
+ "services": "Business logic",
616
+ "core": "Core functionality",
617
+ "lib": "Library code",
618
+ "utils": "Utility functions",
619
+ "templates": "HTML templates",
620
+ "static": "Static assets",
621
+ "tests": "Test suite",
622
+ }
623
+
624
+ return descriptions.get(dir_name, "Code directory")
625
+
626
+ def _file_exists_in_tree(self, root: Path, pattern: str) -> bool:
627
+ """Check if any file matching pattern exists in tree."""
628
+ try:
629
+ for _ in root.rglob(pattern):
630
+ return True
631
+ except Exception:
632
+ pass
633
+ return False
634
+
635
+ def _detect_error_handling(self, root: Path) -> str:
636
+ """Detect error handling patterns."""
637
+ has_middleware = self._file_exists_in_tree(root, "*middleware*.py")
638
+ has_exception_handlers = False
639
+
640
+ for p in root.rglob("*.py"):
641
+ try:
642
+ content = p.read_text(encoding="utf-8", errors="ignore")
643
+ if "@app.exception_handler" in content or "HTTPException" in content:
644
+ has_exception_handlers = True
645
+ break
646
+ except Exception:
647
+ continue
648
+
649
+ if has_middleware or has_exception_handlers:
650
+ return "* Centralized exception handling detected"
651
+ return "* (Error handling pattern not auto-detected)"
652
+
653
+ def _detect_logging(self, root: Path) -> str:
654
+ """Detect logging configuration."""
655
+ for p in root.rglob("*.py"):
656
+ try:
657
+ content = p.read_text(encoding="utf-8", errors="ignore")
658
+ if "import logging" in content or "from logging" in content:
659
+ return "* Python logging module in use"
660
+ except Exception:
661
+ continue
662
+ return "* (Logging not detected)"
663
+
664
+ def _extract_env_vars(self, root: Path) -> dict[str, list[tuple[str, int]]]:
665
+ """
666
+ Extract environment variables used in the project with file+line references.
667
+ Returns dict mapping var name to list of (relative_path, line_number) tuples.
668
+ Filters out OS/toolchain noise.
669
+ """
670
+ # OS/toolchain vars to filter out - these pollute AI context
671
+ OS_NOISE = {
672
+ # CI/CD platforms
673
+ "GITHUB_ACTIONS", "GITHUB_TOKEN", "GITHUB_WORKSPACE", "GITHUB_SHA",
674
+ "CI", "CONTINUOUS_INTEGRATION", "GITLAB_CI", "CIRCLECI", "TRAVIS",
675
+ # Testing frameworks
676
+ "PYTEST_CURRENT_TEST", "PYTEST_VERSION", "PYTEST_TIMEOUT",
677
+ # Python tooling
678
+ "PYTHONPATH", "PYTHONHOME", "PYTHONDONTWRITEBYTECODE", "PYTHONUNBUFFERED",
679
+ "VIRTUAL_ENV", "CONDA_DEFAULT_ENV", "CONDA_PREFIX",
680
+ # Build/compile
681
+ "CC", "CXX", "CFLAGS", "CXXFLAGS", "LDFLAGS", "AR", "NM", "RANLIB",
682
+ # Android SDK (common on dev machines)
683
+ "ANDROID_HOME", "ANDROID_SDK_ROOT", "ANDROID_NDK_HOME",
684
+ # System/OS
685
+ "PATH", "HOME", "USER", "SHELL", "LANG", "LC_ALL", "TZ", "TERM",
686
+ "PWD", "OLDPWD", "SHLVL", "EDITOR", "PAGER",
687
+ # SSL/Security debugging
688
+ "SSLKEYLOGFILE", "SSL_CERT_FILE", "SSL_CERT_DIR",
689
+ # Package managers
690
+ "PIP_INDEX_URL", "PIP_EXTRA_INDEX_URL", "NPM_TOKEN",
691
+ # Display/X11
692
+ "DISPLAY", "XAUTHORITY", "WAYLAND_DISPLAY",
693
+ }
694
+
695
+ env_vars: dict[str, list[tuple[str, int]]] = {}
696
+
697
+ # Patterns to detect env var usage
698
+ import re
699
+ patterns = [
700
+ re.compile(r'os\.getenv\(["\']([A-Z_]+)["\']\)'),
701
+ re.compile(r'os\.environ\[["\']([A-Z_]+)["\']\]'),
702
+ re.compile(r'os\.environ\.get\(["\']([A-Z_]+)["\']\)'),
703
+ ]
704
+
705
+ for p in root.rglob("*.py"):
706
+ if should_exclude_from_analysis(p):
707
+ continue
708
+ try:
709
+ with open(p, "r", encoding="utf-8", errors="ignore") as f:
710
+ for line_num, line in enumerate(f, start=1):
711
+ for pattern in patterns:
712
+ for match in pattern.finditer(line):
713
+ var_name = match.group(1)
714
+ # Filter OS/toolchain noise
715
+ if var_name not in OS_NOISE:
716
+ rel_path = str(p.relative_to(root))
717
+ if var_name not in env_vars:
718
+ env_vars[var_name] = []
719
+ env_vars[var_name].append((rel_path, line_num))
720
+ except Exception:
721
+ continue
722
+
723
+ return env_vars
724
+
725
+ def _detect_database(self, root: Path) -> str | None:
726
+ """Detect database type."""
727
+ for p in root.rglob("*.py"):
728
+ try:
729
+ content = p.read_text(encoding="utf-8", errors="ignore")
730
+ if "postgresql" in content.lower() or "psycopg" in content:
731
+ return "PostgreSQL"
732
+ if "mysql" in content.lower() or "pymysql" in content:
733
+ return "MySQL"
734
+ if "sqlite" in content.lower():
735
+ return "SQLite"
736
+ if "mongodb" in content.lower() or "pymongo" in content:
737
+ return "MongoDB"
738
+ except Exception:
739
+ continue
740
+ return None
741
+
742
+ def _detect_orm(self, root: Path) -> str | None:
743
+ """Detect ORM/data layer."""
744
+ for p in root.rglob("*.py"):
745
+ try:
746
+ content = p.read_text(encoding="utf-8", errors="ignore")
747
+ if "from sqlalchemy" in content or "import sqlalchemy" in content:
748
+ return "SQLAlchemy"
749
+ if "from django.db" in content:
750
+ return "Django ORM"
751
+ if "from tortoise" in content:
752
+ return "Tortoise ORM"
753
+ except Exception:
754
+ continue
755
+ return None
756
+
757
+ def _detect_migrations(self, root: Path) -> str:
758
+ """Detect migration system."""
759
+ if (root / "migrations").is_dir() or (root / "alembic").is_dir():
760
+ return "* Migration system detected (likely Alembic)"
761
+ return "* (No migrations detected)"
762
+
763
+ def _extract_routes(self, root: Path) -> list[str]:
764
+ """Extract API routes from code."""
765
+ routes = []
766
+
767
+ for p in root.rglob("*.py"):
768
+ if any(x in p.parts for x in [".venv", "venv", "__pycache__", "artifacts"]):
769
+ continue
770
+ try:
771
+ content = p.read_text(encoding="utf-8", errors="ignore")
772
+ import re
773
+ # FastAPI patterns
774
+ patterns = [
775
+ r'@\w+\.(get|post|put|delete|patch)\(["\']([^"\']+)["\']\)',
776
+ r'@app\.route\(["\']([^"\']+)["\']\)',
777
+ ]
778
+ for pattern in patterns:
779
+ matches = re.findall(pattern, content)
780
+ for match in matches:
781
+ if isinstance(match, tuple):
782
+ if len(match) == 2:
783
+ routes.append(f"{match[0].upper()} {match[1]}")
784
+ else:
785
+ routes.append(f"GET {match[0]}")
786
+ else:
787
+ routes.append(f"GET {match}")
788
+ except Exception:
789
+ continue
790
+
791
+ return sorted(set(routes))