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,82 @@
1
+ gwc_pybundle-2.1.2.dist-info/licenses/LICENSE.md,sha256=ZmD484KG9hysmSMFT824y7aIc8lhFBnjkN-3DJNjXCc,1108
2
+ pybundle/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ pybundle/__main__.py,sha256=MHKZ_ae3fSLGTLUUMOx15fWdeOnJSHhq-zslRP5F5Lc,79
4
+ pybundle/cli.py,sha256=VvEHrWDC1BM_Y8CkJQDaK0Epzo1v5ljOdoG1djKZNdE,19372
5
+ pybundle/context.py,sha256=LBd0QYUuX_fMdWbV06L6v4TZFkAet2o1h9GS1PN8iYY,13357
6
+ pybundle/doctor.py,sha256=6tpEH5_UVKA8Wbi5kKkUoxXMjTGOrEZr3leBd4pU3Lw,4669
7
+ pybundle/filters.py,sha256=98haWs-EzQlaroVdU-rP-c3661GPwi-hmj119OsgtJQ,4675
8
+ pybundle/manifest.py,sha256=LJB1JXg5kH6nKjgzif-sDWoaaqmIEkF3ctlbHqE0EHQ,2289
9
+ pybundle/packaging.py,sha256=CQ394kUmn6sudpy7ExGpmeanfRVEf_xxwrX_swaotFg,1407
10
+ pybundle/policy.py,sha256=PLk2Fs1hBxfn0jAWE3Jcj1jhlUnwfD5ivOFrUScI8Io,3929
11
+ pybundle/profiles.py,sha256=09iy3-1pIsC4KrHZfDsoFLKhaUewkqX6xurTLPcAks8,14247
12
+ pybundle/roadmap_model.py,sha256=rcMwphd3uNiwzAVx_tGTE20GYYFRzyvbCoFcYGRS9aY,991
13
+ pybundle/roadmap_scan.py,sha256=iVJgaTgFjieTNv_NP_hjOFGJYgE8hOdG3UK0NRUtClU,11223
14
+ pybundle/root_detect.py,sha256=d_HdA_usPxK1orc9cnNTY_fZrvdScQnI_f1XvvlHSFA,361
15
+ pybundle/runner.py,sha256=oG4nUIpUVefcho45_gCdEeywMTBZepOjrciXFeERy-g,5548
16
+ pybundle/tools.py,sha256=dLg6m7tW11QC6SoJUzoayYEI-lq_kKuOm2eN2uzqnQo,1926
17
+ pybundle/steps/__init__.py,sha256=3NL5A0Oj4MlTPW7LvdzesrLHMJG98nV-qrcaJuICE4o,394
18
+ pybundle/steps/ai_context.py,sha256=YYp83eKiHrLI2dAFjlk48i3F9n0BZC_NFle43-PzqGE,27346
19
+ pybundle/steps/api_docs.py,sha256=SXDITOHi2mGEcBZISAn_8K9Jex6baaWUHJnRC22GAr8,8278
20
+ pybundle/steps/asyncio_analysis.py,sha256=LYOY8Iwu-2t4dgBpFn5dzLF_iz2nFdEuWOph6FnAcnw,14249
21
+ pybundle/steps/bandit.py,sha256=4gT9JJerhT9W8O6lF_Vwk6iNjYvJqQcihVcF816NzS4,2439
22
+ pybundle/steps/base.py,sha256=9LZwPYlGAWfnIe5DRbfP89ZmMLhbmOm9SQ-vWmQANHA,356
23
+ pybundle/steps/blocking_call_detection.py,sha256=K9HjSKQ69NbE9TfLvNgFaX1WpNwGZgd2oqxNPc8oLDU,10534
24
+ pybundle/steps/call_graph.py,sha256=zUwZD2FHDmbGlf2JsOUaImjIJW7tPZpo-edXPBBRpo0,7764
25
+ pybundle/steps/compileall.py,sha256=uB_eFVq1FSSrdzZvx9hgs4IbNJnrXzPOCjyTfDw0gsc,2446
26
+ pybundle/steps/config_docs.py,sha256=fsJgf1JqaGBztRsrbsh5K827JXltPYRvM_nYOvrhxVw,11542
27
+ pybundle/steps/config_validation.py,sha256=jmJq0uYAwwt0BSBB6f1TEofm2_iKAHrcl01KwehzN3A,10747
28
+ pybundle/steps/container_image.py,sha256=NkvT-GVEAOmvqgH5B0V67zcP-Y-m8Z-t2h7tDXynZRA,9910
29
+ pybundle/steps/context_expand.py,sha256=pG1-59RqQFIdTlMj5CJ65b8Or9qNk8LgudntdT10m7M,7986
30
+ pybundle/steps/copy_pack.py,sha256=_I_hDBLiG9f9ETFzpG59lOFhe7nOasGPRvQb4CunBlE,9778
31
+ pybundle/steps/coverage.py,sha256=HIMuQH8PFPeoo_bPsIhn3U2UHzes5qiQIyJRqRnz0yI,3600
32
+ pybundle/steps/cprofile_step.py,sha256=WLibVdy4jLSSezdEFkLBwcRsSpWIsbLrX1597Iozy-4,5729
33
+ pybundle/steps/dependency_sizes.py,sha256=DVvPXktnrVloGccFZBy3NdYQblnvRQsgmuzqYheU_Xc,5330
34
+ pybundle/steps/django_checks.py,sha256=TMnLRykEWN24Sep2pn4_5vIl4l36Z9HtuTl3lLFeXtU,7190
35
+ pybundle/steps/dockerfile_lint.py,sha256=nZ4zH3qhBedTHwfUdEq-HkocRA7Alu_gdkj0w_5RE4E,9561
36
+ pybundle/steps/dockerignore.py,sha256=HzZfd4O-6H-bZZyHg1OMbMSad5VEQ2qrdJbHRuzDrnU,10887
37
+ pybundle/steps/duplication.py,sha256=ntQzmIYaCYWG0fCo4i5GI4Yx2jH6Td94p6V94L7W9Ug,3838
38
+ pybundle/steps/env_completeness.py,sha256=qu28TMuwuSGuEAirDXgnNUT2-iJIunVMwJFOEYtI32U,9747
39
+ pybundle/steps/env_var_usage.py,sha256=XcmSzkXkQGJYVKn0bkJ2Yi67ITQjDKvQBlKM6ekNhmg,8985
40
+ pybundle/steps/error_refs.py,sha256=mq1DGXREQRudycyPb7H0obkHmv3HvO-QPPbf2zoZDu0,5640
41
+ pybundle/steps/event_loop_patterns.py,sha256=EYHhHnpanPk1AHjudcoeVhPdeihxnVMVx9wwUbjto18,10675
42
+ pybundle/steps/exception_patterns.py,sha256=8dPdnrZ1W2DrVwJvJjCUB1HzUrfhrujKu6K1cLCAegE,6694
43
+ pybundle/steps/fastapi_integration.py,sha256=38JQpjoUWc2zJEQqp99p7ZygBcELNj3xbN7LuVei4_w,8942
44
+ pybundle/steps/flask_debugging.py,sha256=Jrvjvgp7-gV2mfygbh-EYZwhwO8fzLKv_Ob7R1ovNAQ,10797
45
+ pybundle/steps/git_analytics.py,sha256=_N2Fo8NtcNyFepJ6nqaYVdL7tYmVwdnyjh4tYwf73eM,11438
46
+ pybundle/steps/handoff_md.py,sha256=3gl0is27Iumdh-g9pzsYT1KaRPUOeq2BfZ6KhbolT-w,6739
47
+ pybundle/steps/import_time.py,sha256=JwLR3DXrZMhLzhVTJYEYnvOhNXfhpNblsKiUHKRdR-o,6292
48
+ pybundle/steps/interrogate.py,sha256=O-oNOWZ1NXWbNS3bV7-v3fADurl6c7GwyaHgfu7CDIY,3525
49
+ pybundle/steps/license_scan.py,sha256=Bbo1FEokrbSbIzbxAkGo2WWZTEUFeWYOW3y-UMY1EfU,3559
50
+ pybundle/steps/line_profiler.py,sha256=c8vyOiMKXCaU_7qA58wyi7K_8lenRXJF5zPC7g_Lw1g,4164
51
+ pybundle/steps/link_validation.py,sha256=tWYMnFurF4xzS-khSJCpFpAtadgLxLRTIB8eLPHUteU,10266
52
+ pybundle/steps/logging_analysis.py,sha256=S_Srl0FM7QphXRB8B_OEe3ujh6bFIUIyxOAGi0O2-4o,7932
53
+ pybundle/steps/memory_profile.py,sha256=-DiRvEs6vViQCyvzyM-2YeV27aT0X2zxF94Y6aZGHvM,5243
54
+ pybundle/steps/migration_history.py,sha256=5cfgdxeGYU9E108rd1erH4c3dKlM-FBlDlRpRTs6Dz4,12320
55
+ pybundle/steps/mutation_testing.py,sha256=glqLsVOEpdt1phEpYjzdsDbqdefUse8sgFz9migEAAw,5301
56
+ pybundle/steps/mypy.py,sha256=hA-20H-Vv9BI_u0fgstnANXnItoD36dSJ4J5aXTCLKk,3705
57
+ pybundle/steps/orm_optimization.py,sha256=gLKACtOsyy3EiDUzs9lFcZU0ZVk83DXYmtVwKhzIVWA,11560
58
+ pybundle/steps/pip_audit.py,sha256=qNkHeyjzUFJ9SSKU4jTKOsUnWnNYAk_jWcmPlUckkAA,1582
59
+ pybundle/steps/pipdeptree.py,sha256=cD1PppGXQ9dSaZ1PpYI-066JeNqXQThQyJ0kw1etw30,2125
60
+ pybundle/steps/pylance.py,sha256=PiIyyYm8gTVv-dihzRjvD4waZnz31qNOEAjYIlscvSY,15715
61
+ pybundle/steps/pytest.py,sha256=vfr2DI6aYCTit_j9QdV3WtKlOZtLMCCSm2E__3tsZt4,2177
62
+ pybundle/steps/query_pattern_analysis.py,sha256=4oXy4_vyQQ3UrrwakJYu4ExAJc9lT6MiVduFW76-WBw,12608
63
+ pybundle/steps/radon.py,sha256=5zeNienQJoIOCJh3aNMlFjRXzCU8cw6cuqX26bO8nHk,5256
64
+ pybundle/steps/repro_md.py,sha256=EvoNVKaHFelfrv_jfBLpbkORTiDSjah3GgWXW3NvEXY,4856
65
+ pybundle/steps/rg_scans.py,sha256=hcHhzPAJ_MdPthRLcfIzxlkXoQJkDF2bIX2wU1By2gU,2511
66
+ pybundle/steps/roadmap.py,sha256=gFS2Mo9CR1A8wg6xNVP_FnJuT_dm8yWjgRJbtlyN2wk,5213
67
+ pybundle/steps/ruff.py,sha256=Furb9nx0xMkb270jtrZiYdZ5pNB6J4iOvKRtxj82_aE,4057
68
+ pybundle/steps/secrets_detection.py,sha256=SKphNUYDL1a-Ui1BEI8dOPsY8YXDqAENv6f4TEZspB8,8983
69
+ pybundle/steps/security_headers.py,sha256=u1RC-lDfNL7JeUa2yMHYkmiDkDbS83Db7LXFEXpRKKU,11244
70
+ pybundle/steps/shell.py,sha256=vYGu9fZ5gDdZnKSfok95bOTIWu5HNvS23atsY5FwtJE,2529
71
+ pybundle/steps/slow_tests.py,sha256=E21xOcUy85MNp9n4bNuYzAtRxOnbG_gUHoi2Tjs4JHc,6891
72
+ pybundle/steps/sqlalchemy_validation.py,sha256=cgqKI4dx2yNp5Q9uL_q_O26pUUxwCaCi5gCOH8fzKVo,9542
73
+ pybundle/steps/test_flakiness.py,sha256=-OiAtUWFA2xp67RcC5D6jUlnqGN_E_Zw-5yfLET1KuM,7162
74
+ pybundle/steps/tree.py,sha256=tz5Z7clOEuKWMW6wjj2xtEu3pNjEto8dYgHYaoDdM0E,3674
75
+ pybundle/steps/type_coverage.py,sha256=7AF0FLqVifPmj17mEYuGcehpLrYz3-tFv-0e5ZCPxiA,9684
76
+ pybundle/steps/unused_deps.py,sha256=pPGPGY2F6P3cGDl1mR6L1-mYxlJ_xu92GQSaFmJhTkw,8263
77
+ pybundle/steps/vulture.py,sha256=afanCBH3YZ-89HEH8UGd-uJzDnTUO_jKss-6jZQCcE0,6137
78
+ gwc_pybundle-2.1.2.dist-info/METADATA,sha256=zKrvyqzho74sgrlZE8nD2blW1kN_Pmz3NJlHKOpgs-w,26454
79
+ gwc_pybundle-2.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
80
+ gwc_pybundle-2.1.2.dist-info/entry_points.txt,sha256=xgXfIvyx9ZI6jd9MscFWNMzQrcX4scTVJ9L7WwksUHg,47
81
+ gwc_pybundle-2.1.2.dist-info/top_level.txt,sha256=N5x3QutDUtHQn3HwkFmH5PeM9uPY-E5wOKUJSE8PBKM,9
82
+ gwc_pybundle-2.1.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pybundle = pybundle.cli:main
@@ -0,0 +1,25 @@
1
+ The MIT License (MIT)
2
+ =====================
3
+
4
+ Copyright © 2025 Jessica Brown
5
+
6
+ Permission is hereby granted, free of charge, to any person
7
+ obtaining a copy of this software and associated documentation
8
+ files (the “Software”), to deal in the Software without
9
+ restriction, including without limitation the rights to use,
10
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the
12
+ Software is furnished to do so, subject to the following
13
+ conditions:
14
+
15
+ The above copyright notice and this permission notice shall be
16
+ included in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
19
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
20
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
22
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
23
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
25
+ OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1 @@
1
+ pybundle
pybundle/__init__.py ADDED
File without changes
pybundle/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
pybundle/cli.py ADDED
@@ -0,0 +1,546 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+ import shlex
6
+
7
+ from .context import BundleContext, RunOptions
8
+ from .profiles import get_profile
9
+ from .root_detect import detect_project_root
10
+ from importlib.metadata import PackageNotFoundError, version as pkg_version
11
+ from .runner import run_profile
12
+
13
+
14
+ def get_version() -> str:
15
+ # 1) Canonical for installed distributions (including editable)
16
+ try:
17
+ return pkg_version("gwc-pybundle")
18
+ except PackageNotFoundError:
19
+ pass
20
+
21
+ # 2) Dev fallback: locate pyproject.toml by walking up from this file
22
+ try:
23
+ import tomllib # py3.11+
24
+ except Exception:
25
+ return "unknown"
26
+
27
+ here = Path(__file__).resolve()
28
+ for parent in [here.parent] + list(here.parents):
29
+ pp = parent / "pyproject.toml"
30
+ if pp.is_file():
31
+ try:
32
+ data = tomllib.loads(pp.read_text(encoding="utf-8"))
33
+ return data.get("project", {}).get("version", "unknown")
34
+ except Exception:
35
+ return "unknown"
36
+
37
+ return "unknown"
38
+
39
+
40
+ def add_common_args(sp: argparse.ArgumentParser) -> None:
41
+ sp.add_argument(
42
+ "--project-root",
43
+ type=Path,
44
+ default=None,
45
+ help="Explicit project root (skip auto-detect)",
46
+ )
47
+ sp.add_argument(
48
+ "--outdir",
49
+ type=Path,
50
+ default=None,
51
+ help="Output directory (default: <root>/artifacts)",
52
+ )
53
+ sp.add_argument("--name", default=None, help="Override archive name prefix")
54
+ sp.add_argument(
55
+ "--strict", action="store_true", help="Fail non-zero if any step fails"
56
+ )
57
+ sp.add_argument(
58
+ "--redact",
59
+ action=argparse.BooleanOptionalAction,
60
+ default=True,
61
+ help="Redact secrets in logs/text",
62
+ )
63
+ sp.add_argument(
64
+ "--json",
65
+ action="store_true",
66
+ help="Emit machine-readable JSON to stdout.",
67
+ )
68
+
69
+
70
+ def _resolve_profile_defaults(profile: str, o: RunOptions) -> RunOptions:
71
+ if profile == "ai":
72
+ # AI defaults: skip slow/flake-prone tools unless explicitly enabled
73
+ return RunOptions(
74
+ **{
75
+ **o.__dict__,
76
+ "no_ruff": o.no_ruff if o.no_ruff is not None else True,
77
+ "no_mypy": o.no_mypy if o.no_mypy is not None else True,
78
+ "no_pytest": o.no_pytest if o.no_pytest is not None else True,
79
+ "no_rg": o.no_rg if o.no_rg is not None else True,
80
+ "no_error_refs": o.no_error_refs
81
+ if o.no_error_refs is not None
82
+ else True,
83
+ "no_context": o.no_context if o.no_context is not None else True,
84
+ }
85
+ )
86
+ return o
87
+
88
+
89
+ def add_run_only_args(sp: argparse.ArgumentParser) -> None:
90
+ sp.add_argument(
91
+ "--format",
92
+ choices=["auto", "zip", "tar.gz"],
93
+ default="auto",
94
+ help="Archive format",
95
+ )
96
+ sp.add_argument(
97
+ "--clean-workdir",
98
+ action="store_true",
99
+ help="Delete expanded workdir after packaging",
100
+ )
101
+
102
+
103
+ def add_knobs(sp: argparse.ArgumentParser) -> None:
104
+ # selective skips
105
+ sp.add_argument("--ruff", dest="no_ruff", action="store_false", default=None)
106
+ sp.add_argument("--no-ruff", dest="no_ruff", action="store_true", default=None)
107
+ sp.add_argument("--mypy", dest="no_mypy", action="store_false", default=None)
108
+ sp.add_argument("--no-mypy", dest="no_mypy", action="store_true", default=None)
109
+ sp.add_argument("--pylance", dest="no_pylance", action="store_false", default=None)
110
+ sp.add_argument(
111
+ "--no-pylance", dest="no_pylance", action="store_true", default=None
112
+ )
113
+ sp.add_argument("--pytest", dest="no_pytest", action="store_false", default=None)
114
+ sp.add_argument("--no-pytest", dest="no_pytest", action="store_true", default=None)
115
+ sp.add_argument("--bandit", dest="no_bandit", action="store_false", default=None)
116
+ sp.add_argument("--no-bandit", dest="no_bandit", action="store_true", default=None)
117
+ sp.add_argument(
118
+ "--pip-audit", dest="no_pip_audit", action="store_false", default=None
119
+ )
120
+ sp.add_argument(
121
+ "--no-pip-audit", dest="no_pip_audit", action="store_true", default=None
122
+ )
123
+ sp.add_argument(
124
+ "--coverage", dest="no_coverage", action="store_false", default=None
125
+ )
126
+ sp.add_argument(
127
+ "--no-coverage", dest="no_coverage", action="store_true", default=None
128
+ )
129
+ sp.add_argument("--rg", dest="no_rg", action="store_false", default=None)
130
+ sp.add_argument("--no-rg", dest="no_rg", action="store_true", default=None)
131
+ sp.add_argument(
132
+ "--error-refs", dest="no_error_refs", action="store_false", default=None
133
+ )
134
+ sp.add_argument(
135
+ "--no-error-refs", dest="no_error_refs", action="store_true", default=None
136
+ )
137
+ sp.add_argument("--context", dest="no_context", action="store_false", default=None)
138
+ sp.add_argument(
139
+ "--no-context", dest="no_context", action="store_true", default=None
140
+ )
141
+
142
+ # code quality tools (v1.3.0)
143
+ sp.add_argument("--vulture", dest="no_vulture", action="store_false", default=None)
144
+ sp.add_argument(
145
+ "--no-vulture", dest="no_vulture", action="store_true", default=None
146
+ )
147
+ sp.add_argument("--radon", dest="no_radon", action="store_false", default=None)
148
+ sp.add_argument("--no-radon", dest="no_radon", action="store_true", default=None)
149
+ sp.add_argument(
150
+ "--interrogate", dest="no_interrogate", action="store_false", default=None
151
+ )
152
+ sp.add_argument(
153
+ "--no-interrogate", dest="no_interrogate", action="store_true", default=None
154
+ )
155
+ sp.add_argument(
156
+ "--duplication", dest="no_duplication", action="store_false", default=None
157
+ )
158
+ sp.add_argument(
159
+ "--no-duplication", dest="no_duplication", action="store_true", default=None
160
+ )
161
+
162
+ # dependency analysis tools (v1.3.1)
163
+ sp.add_argument(
164
+ "--pipdeptree", dest="no_pipdeptree", action="store_false", default=None
165
+ )
166
+ sp.add_argument(
167
+ "--no-pipdeptree", dest="no_pipdeptree", action="store_true", default=None
168
+ )
169
+ sp.add_argument(
170
+ "--unused-deps", dest="no_unused_deps", action="store_false", default=None
171
+ )
172
+ sp.add_argument(
173
+ "--no-unused-deps", dest="no_unused_deps", action="store_true", default=None
174
+ )
175
+ sp.add_argument(
176
+ "--license-scan", dest="no_license_scan", action="store_false", default=None
177
+ )
178
+ sp.add_argument(
179
+ "--no-license-scan", dest="no_license_scan", action="store_true", default=None
180
+ )
181
+ sp.add_argument(
182
+ "--dependency-sizes",
183
+ dest="no_dependency_sizes",
184
+ action="store_false",
185
+ default=None,
186
+ )
187
+ sp.add_argument(
188
+ "--no-dependency-sizes",
189
+ dest="no_dependency_sizes",
190
+ action="store_true",
191
+ default=None,
192
+ )
193
+
194
+ # performance profiling (v1.4.0)
195
+ sp.add_argument("--profile", dest="no_profile", action="store_false", default=None)
196
+ sp.add_argument(
197
+ "--no-profile", dest="no_profile", action="store_true", default=None
198
+ )
199
+ sp.add_argument(
200
+ "--profile-entry-point",
201
+ type=str,
202
+ default=None,
203
+ help="Entry point for profiling (e.g., main.py or tests/)",
204
+ )
205
+ sp.add_argument(
206
+ "--profile-memory",
207
+ action="store_true",
208
+ default=False,
209
+ help="Enable memory profiling with tracemalloc",
210
+ )
211
+ sp.add_argument(
212
+ "--enable-line-profiler",
213
+ action="store_true",
214
+ default=False,
215
+ help="Enable line_profiler (requires @profile decorators)",
216
+ )
217
+
218
+ # test quality & coverage (v1.4.1)
219
+ sp.add_argument(
220
+ "--test-flakiness-runs",
221
+ type=int,
222
+ default=3,
223
+ help="Number of times to run tests for flakiness detection (default: 3)",
224
+ )
225
+ sp.add_argument(
226
+ "--slow-test-threshold",
227
+ type=float,
228
+ default=1.0,
229
+ help="Threshold in seconds for identifying slow tests (default: 1.0)",
230
+ )
231
+ sp.add_argument(
232
+ "--mutation",
233
+ dest="enable_mutation_testing",
234
+ action="store_true",
235
+ default=False,
236
+ help="Enable mutation testing with mutmut (VERY SLOW!)",
237
+ )
238
+ sp.add_argument(
239
+ "--no-mutation",
240
+ dest="enable_mutation_testing",
241
+ action="store_false",
242
+ default=False,
243
+ help="Disable mutation testing (default)",
244
+ )
245
+
246
+ # documentation & type quality (v1.5.0)
247
+ sp.add_argument(
248
+ "--type-coverage", dest="no_type_coverage", action="store_false", default=None
249
+ )
250
+ sp.add_argument(
251
+ "--no-type-coverage", dest="no_type_coverage", action="store_true", default=None
252
+ )
253
+ sp.add_argument(
254
+ "--link-check", dest="no_link_check", action="store_false", default=None
255
+ )
256
+ sp.add_argument(
257
+ "--no-link-check", dest="no_link_check", action="store_true", default=None
258
+ )
259
+ sp.add_argument(
260
+ "--api-docs", dest="no_api_docs", action="store_false", default=None
261
+ )
262
+ sp.add_argument(
263
+ "--no-api-docs", dest="no_api_docs", action="store_true", default=None
264
+ )
265
+ sp.add_argument(
266
+ "--config-docs", dest="no_config_docs", action="store_false", default=None
267
+ )
268
+ sp.add_argument(
269
+ "--no-config-docs", dest="no_config_docs", action="store_true", default=None
270
+ )
271
+
272
+ # Git analytics options (v1.5.1)
273
+ sp.add_argument(
274
+ "--git-analytics", dest="no_git_analytics", action="store_false", default=None
275
+ )
276
+ sp.add_argument(
277
+ "--no-git-analytics", dest="no_git_analytics", action="store_true", default=None
278
+ )
279
+ sp.add_argument(
280
+ "--git-blame-depth",
281
+ type=int,
282
+ default=100,
283
+ help="Number of commits to analyze in git blame (default: 100)",
284
+ )
285
+
286
+ # Runtime analysis options (v1.5.2)
287
+ sp.add_argument(
288
+ "--runtime-analysis", dest="no_runtime_analysis", action="store_false", default=None
289
+ )
290
+ sp.add_argument(
291
+ "--no-runtime-analysis", dest="no_runtime_analysis", action="store_true", default=None
292
+ )
293
+
294
+ # Container & deployment options (v2.0.0)
295
+ sp.add_argument(
296
+ "--container-analysis", dest="no_container_analysis", action="store_false", default=None
297
+ )
298
+ sp.add_argument(
299
+ "--no-container-analysis", dest="no_container_analysis", action="store_true", default=None
300
+ )
301
+ sp.add_argument(
302
+ "--docker-image",
303
+ type=str,
304
+ default=None,
305
+ help="Docker image name for analysis (default: auto-detect from docker-compose)",
306
+ )
307
+
308
+ # Configuration & security options (v2.0.0)
309
+ sp.add_argument(
310
+ "--config-security-analysis", dest="no_config_security_analysis", action="store_false", default=None
311
+ )
312
+ sp.add_argument(
313
+ "--no-config-security-analysis", dest="no_config_security_analysis", action="store_true", default=None
314
+ )
315
+
316
+ # Async & modern Python options (v2.0.0)
317
+ sp.add_argument(
318
+ "--async-analysis", dest="no_async_analysis", action="store_false", default=None
319
+ )
320
+ sp.add_argument(
321
+ "--no-async-analysis", dest="no_async_analysis", action="store_true", default=None
322
+ )
323
+
324
+ # Database & data layer options (v2.0.0)
325
+ sp.add_argument(
326
+ "--db-analysis", dest="no_db_analysis", action="store_false", default=None
327
+ )
328
+ sp.add_argument(
329
+ "--no-db-analysis", dest="no_db_analysis", action="store_true", default=None
330
+ )
331
+
332
+ # Framework-specific options (v2.0.0)
333
+ sp.add_argument(
334
+ "--framework-analysis", dest="no_framework_analysis", action="store_false", default=None
335
+ )
336
+ sp.add_argument(
337
+ "--no-framework-analysis", dest="no_framework_analysis", action="store_true", default=None
338
+ )
339
+
340
+ # Security options
341
+ sp.add_argument(
342
+ "--strict-paths",
343
+ action="store_true",
344
+ help="Only use tools from trusted system directories (enhanced security)",
345
+ )
346
+
347
+ # targets / args
348
+ sp.add_argument("--ruff-target", default=".")
349
+ sp.add_argument("--mypy-target", default=".")
350
+ sp.add_argument(
351
+ "--pytest-args",
352
+ default="-q",
353
+ help='Pytest args as a single string, e.g. "--maxfail=1 -q"',
354
+ )
355
+
356
+ # caps
357
+ sp.add_argument("--error-max-files", type=int, default=250)
358
+ sp.add_argument("--context-depth", type=int, default=2)
359
+ sp.add_argument("--context-max-files", type=int, default=600)
360
+
361
+
362
+ def build_parser() -> argparse.ArgumentParser:
363
+ p = argparse.ArgumentParser(
364
+ prog="pybundle", description="Build portable diagnostic bundles for projects."
365
+ )
366
+ sub = p.add_subparsers(dest="cmd", required=True)
367
+ sub.add_parser("version", help="Show version")
368
+ sub.add_parser("list-profiles", help="List available profiles")
369
+
370
+ runp = sub.add_parser("run", help="Run a profile and build an archive")
371
+ runp.add_argument("profile", choices=["analysis", "debug", "backup", "ai"])
372
+ add_common_args(runp)
373
+ add_run_only_args(runp)
374
+ add_knobs(runp)
375
+
376
+ docp = sub.add_parser("doctor", help="Show tool availability and what would run")
377
+ docp.add_argument(
378
+ "profile",
379
+ choices=["analysis", "debug", "backup", "ai"],
380
+ nargs="?",
381
+ default="analysis",
382
+ )
383
+ add_common_args(docp)
384
+ add_knobs(docp)
385
+
386
+ return p
387
+
388
+
389
+ def _build_options(args) -> RunOptions:
390
+ pytest_args = (
391
+ shlex.split(args.pytest_args) if getattr(args, "pytest_args", None) else ["-q"]
392
+ )
393
+ return RunOptions(
394
+ no_ruff=getattr(args, "no_ruff", None),
395
+ no_mypy=getattr(args, "no_mypy", None),
396
+ no_pylance=getattr(args, "no_pylance", None),
397
+ no_pytest=getattr(args, "no_pytest", None),
398
+ no_bandit=getattr(args, "no_bandit", None),
399
+ no_pip_audit=getattr(args, "no_pip_audit", None),
400
+ no_coverage=getattr(args, "no_coverage", None),
401
+ no_rg=getattr(args, "no_rg", None),
402
+ no_error_refs=getattr(args, "no_error_refs", None),
403
+ no_context=getattr(args, "no_context", None),
404
+ no_compileall=getattr(args, "no_compileall", None),
405
+ no_vulture=getattr(args, "no_vulture", None),
406
+ no_radon=getattr(args, "no_radon", None),
407
+ no_interrogate=getattr(args, "no_interrogate", None),
408
+ no_duplication=getattr(args, "no_duplication", None),
409
+ no_pipdeptree=getattr(args, "no_pipdeptree", None),
410
+ no_unused_deps=getattr(args, "no_unused_deps", None),
411
+ no_license_scan=getattr(args, "no_license_scan", None),
412
+ no_dependency_sizes=getattr(args, "no_dependency_sizes", None),
413
+ no_profile=getattr(args, "no_profile", None),
414
+ profile_entry_point=getattr(args, "profile_entry_point", None),
415
+ profile_memory=getattr(args, "profile_memory", False),
416
+ enable_line_profiler=getattr(args, "enable_line_profiler", False),
417
+ test_flakiness_runs=getattr(args, "test_flakiness_runs", 3),
418
+ slow_test_threshold=getattr(args, "slow_test_threshold", 1.0),
419
+ enable_mutation_testing=getattr(args, "enable_mutation_testing", False),
420
+ no_type_coverage=getattr(args, "no_type_coverage", None),
421
+ no_link_check=getattr(args, "no_link_check", None),
422
+ no_api_docs=getattr(args, "no_api_docs", None),
423
+ no_config_docs=getattr(args, "no_config_docs", None),
424
+ no_git_analytics=getattr(args, "no_git_analytics", None),
425
+ git_blame_depth=getattr(args, "git_blame_depth", 100),
426
+ no_runtime_analysis=getattr(args, "no_runtime_analysis", None),
427
+ no_container_analysis=getattr(args, "no_container_analysis", None),
428
+ docker_image=getattr(args, "docker_image", None),
429
+ no_config_security_analysis=getattr(args, "no_config_security_analysis", None),
430
+ no_async_analysis=getattr(args, "no_async_analysis", None),
431
+ no_db_analysis=getattr(args, "no_db_analysis", None),
432
+ no_framework_analysis=getattr(args, "no_framework_analysis", None),
433
+ strict_paths=getattr(args, "strict_paths", False),
434
+ ruff_target=getattr(args, "ruff_target", "."),
435
+ mypy_target=getattr(args, "mypy_target", "."),
436
+ pytest_args=pytest_args,
437
+ error_max_files=getattr(args, "error_max_files", 250),
438
+ context_depth=getattr(args, "context_depth", 2),
439
+ context_max_files=getattr(args, "context_max_files", 600),
440
+ )
441
+
442
+
443
+ def main(argv: list[str] | None = None) -> int:
444
+ import sys
445
+
446
+ # Capture full command for reproducibility before parsing
447
+ if argv is None:
448
+ command_line = ' '.join(sys.argv)
449
+ else:
450
+ command_line = 'pybundle ' + ' '.join(argv)
451
+
452
+ args = build_parser().parse_args(argv)
453
+
454
+ if args.cmd == "version":
455
+ print(f"pybundle {get_version()}")
456
+ return 0
457
+
458
+ if args.cmd == "list-profiles":
459
+ print("ai - AI-friendly context bundle (fast, low-flake defaults)")
460
+ print("backup - portable snapshot (scaffold)")
461
+ print("analysis - neutral diagnostic bundle (humans, tools, assistants)")
462
+ print("debug - deeper diagnostics for developers")
463
+ return 0
464
+
465
+ # run + doctor need a root
466
+ root = args.project_root or detect_project_root(Path.cwd())
467
+ if root is None:
468
+ print("❌ Could not detect project root. Use --project-root PATH.")
469
+ return 20
470
+
471
+ outdir = args.outdir or (root / "artifacts")
472
+
473
+ options = _resolve_profile_defaults(args.profile, _build_options(args))
474
+ profile = get_profile(args.profile, options)
475
+
476
+ if args.cmd == "doctor":
477
+ ctx = BundleContext.create(
478
+ root=root,
479
+ outdir=outdir,
480
+ profile_name=args.profile,
481
+ archive_format="auto",
482
+ name_prefix=args.name,
483
+ strict=args.strict,
484
+ redact=args.redact,
485
+ json_mode=args.json,
486
+ keep_workdir=True,
487
+ options=options,
488
+ )
489
+ ctx.command_used = command_line
490
+
491
+ if args.json:
492
+ ctx.emit_json(ctx.doctor_report(profile))
493
+ else:
494
+ ctx.print_doctor(profile)
495
+ return 0
496
+
497
+ # cmd == run
498
+ keep_workdir = not args.clean_workdir
499
+
500
+ ctx = BundleContext.create(
501
+ root=root,
502
+ outdir=outdir,
503
+ profile_name=args.profile,
504
+ archive_format=args.format,
505
+ name_prefix=args.name,
506
+ strict=args.strict,
507
+ redact=args.redact,
508
+ json_mode=args.json,
509
+ keep_workdir=keep_workdir,
510
+ options=options,
511
+ )
512
+ ctx.command_used = command_line
513
+
514
+ rc = run_profile(ctx, profile)
515
+
516
+ if args.json:
517
+ copied = None
518
+ excluded = None
519
+
520
+ mf = ctx.metadir / "50_copy_manifest.txt"
521
+ if mf.exists():
522
+ data: dict[str, str] = {}
523
+ for line in mf.read_text(encoding="utf-8").splitlines():
524
+ if "=" in line:
525
+ k, v = line.split("=", 1)
526
+ data[k.strip()] = v.strip()
527
+
528
+ copied = int(data.get("copied_files", "0"))
529
+ excluded = int(data.get("excluded_files", "0"))
530
+
531
+ payload = {
532
+ "status": "ok" if rc == 0 else "fail",
533
+ "command": "run",
534
+ "profile": profile.name,
535
+ "files_included": copied if copied is not None else 0,
536
+ "files_excluded": excluded if excluded is not None else 0,
537
+ "duration_ms": ctx.duration_ms or 0,
538
+ "bundle_path": str(ctx.archive_path) if ctx.archive_path else None,
539
+ }
540
+ ctx.emit_json(payload)
541
+
542
+ return rc
543
+
544
+
545
+ if __name__ == "__main__":
546
+ raise SystemExit(main())