python-qv 0.1.2__py3-none-any.whl → 0.1.4__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.
- {python_qv-0.1.2.dist-info → python_qv-0.1.4.dist-info}/METADATA +19 -10
- {python_qv-0.1.2.dist-info → python_qv-0.1.4.dist-info}/RECORD +13 -9
- python_qv-0.1.4.dist-info/licenses/LICENSE +21 -0
- qv/__init__.py +1 -1
- qv/cli/main.py +84 -0
- qv/core/engine.py +4 -0
- qv/frameworks/__init__.py +19 -1
- qv/frameworks/base.py +25 -0
- qv/frameworks/fastapi.py +2219 -0
- qv/frameworks/sqlalchemy.py +1580 -0
- qv/rules/registry.py +614 -0
- {python_qv-0.1.2.dist-info → python_qv-0.1.4.dist-info}/WHEEL +0 -0
- {python_qv-0.1.2.dist-info → python_qv-0.1.4.dist-info}/entry_points.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-qv
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Diagnose why a Python project is unhealthy — explain root cause and suggest safe fixes.
|
|
5
5
|
Project-URL: Homepage, https://github.com/inzamol/qv
|
|
6
6
|
Project-URL: Repository, https://github.com/inzamol/qv
|
|
@@ -8,6 +8,7 @@ Project-URL: Documentation, https://inzamol.github.io/qv/
|
|
|
8
8
|
Project-URL: Issues, https://github.com/inzamol/qv/issues
|
|
9
9
|
Author: Inzamul Hoque
|
|
10
10
|
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
11
12
|
Classifier: Development Status :: 3 - Alpha
|
|
12
13
|
Classifier: Intended Audience :: Developers
|
|
13
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -49,7 +50,7 @@ Description-Content-Type: text/markdown
|
|
|
49
50
|
[](https://github.com/inzamol/qv/actions/workflows/ci.yml)
|
|
50
51
|
[](https://opensource.org/licenses/MIT)
|
|
51
52
|
|
|
52
|
-
[Installation](#-installation) • [Quick Start](#-quick-start) • [Interactive TUI](#-interactive-tui-explorer) • [Features](#-what-it-detects) • [CLI Commands](#-cli-commands) • [CI/CD Integration](#-cicd-integration) • [Documentation](
|
|
53
|
+
[Installation](#-installation) • [Quick Start](#-quick-start) • [Interactive TUI](#-interactive-tui-explorer) • [Features](#-what-it-detects) • [CLI Commands](#-cli-commands) • [CI/CD Integration](#-cicd-integration) • [Documentation](https://inzamol.github.io/qv/)
|
|
53
54
|
|
|
54
55
|
</div>
|
|
55
56
|
|
|
@@ -199,8 +200,10 @@ Run `qv inspect` (or `qv ui`) for a full terminal dashboard:
|
|
|
199
200
|
| | `IMP-002` | Unresolved relative or internal module imports | `ERROR` |
|
|
200
201
|
| **Packaging** | `PKG-001` | Missing PEP 621 metadata (name, version, etc.) | `WARNING` |
|
|
201
202
|
| | `PKG-002` | Invalid syntax or malformed keys in `pyproject.toml` | `ERROR` |
|
|
203
|
+
| **FastAPI Doctor** | `FAP-001`–`FAP-038` | Async blocking calls, CPU starvation in endpoints, insecure CORS, missing timeouts, lifecycle anti-patterns, Pydantic v2 migrations | `ERROR` / `WARNING` |
|
|
204
|
+
| **SQL & Database** | `SQL-001`–`SQL-030` | N+1 queries in loops, session leaks, SQL injection, sync DB in async loop, missing eager loading, pool starvation, 2.0 syntax | `ERROR` / `WARNING` |
|
|
202
205
|
|
|
203
|
-
👉 *See
|
|
206
|
+
👉 *See all 80+ rules and remediation steps in the [Rules Catalog](docs/rules.md).*
|
|
204
207
|
|
|
205
208
|
---
|
|
206
209
|
|
|
@@ -331,6 +334,11 @@ qv environment
|
|
|
331
334
|
|
|
332
335
|
# Check AST & imports only (circular import loops, unresolvable modules)
|
|
333
336
|
qv architecture
|
|
337
|
+
|
|
338
|
+
# Check framework-specific issues (FastAPI, SQLAlchemy, SQLModel)
|
|
339
|
+
qv framework
|
|
340
|
+
qv framework --name fastapi
|
|
341
|
+
qv framework --name sqlalchemy
|
|
334
342
|
```
|
|
335
343
|
|
|
336
344
|
---
|
|
@@ -477,13 +485,14 @@ repos:
|
|
|
477
485
|
---
|
|
478
486
|
|
|
479
487
|
## 📚 Complete Documentation
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
-
|
|
483
|
-
-
|
|
484
|
-
-
|
|
485
|
-
-
|
|
486
|
-
-
|
|
488
|
+
Online documentation is available at **[inzamol.github.io/qv](https://inzamol.github.io/qv/)**.
|
|
489
|
+
|
|
490
|
+
- 🚀 [Getting Started Guide](https://inzamol.github.io/qv/getting_started/)
|
|
491
|
+
- 📖 [CLI Reference](https://inzamol.github.io/qv/cli_reference/)
|
|
492
|
+
- 📋 [Diagnostic Rules Catalog](https://inzamol.github.io/qv/rules/)
|
|
493
|
+
- ⚙️ [Configuration Guide](https://inzamol.github.io/qv/configuration/)
|
|
494
|
+
- 🤖 [CI/CD & SARIF Integration](https://inzamol.github.io/qv/ci_integration/)
|
|
495
|
+
- 🤝 [Contributing Guidelines](https://inzamol.github.io/qv/contributing/)
|
|
487
496
|
|
|
488
497
|
---
|
|
489
498
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
qv/__init__.py,sha256=
|
|
1
|
+
qv/__init__.py,sha256=XbmMZXW29dUN4PoZL_Q2RaC2sEqJ3Uk7VeM2iw3ZNpo,100
|
|
2
2
|
qv/__main__.py,sha256=tG1eQDuox_m4xSLYLEC8KXwxKhL-IJno7CsCgdrw9Bs,112
|
|
3
3
|
qv/analyzers/__init__.py,sha256=_d-1_NpNFMDu1cT-SqTKRBBeipboWZlRt5zZ0tsVPk0,25
|
|
4
4
|
qv/analyzers/ci/__init__.py,sha256=W0Kr4IJBwjysEJPY-D3wNeDMWVFOFQzZt71LIzRDGfY,20
|
|
@@ -16,14 +16,17 @@ qv/analyzers/security/analyzer.py,sha256=DQAm3kex4v63ta8mFeI0gs4OCDvQYPfECLv1vza
|
|
|
16
16
|
qv/analyzers/security/lockfile_parser.py,sha256=XkrJ-bwkv82wIojFsyzkqe4flCJWIliS1DEJNj9E59M,6565
|
|
17
17
|
qv/analyzers/security/osv_client.py,sha256=Y9s8VeY3dSpUd-VBqOvdSSB8KgBzeti1K8niJY2G8IM,4961
|
|
18
18
|
qv/cli/__init__.py,sha256=EUnPKm0L_oVbU1jAO_Icle4TXMCVGpd_aFTQ4iaueik,88
|
|
19
|
-
qv/cli/main.py,sha256=
|
|
19
|
+
qv/cli/main.py,sha256=s4QT4HEkDroVZhuzdKokGzGImiEqqNOSscZIAKZswZM,21839
|
|
20
20
|
qv/core/analyzer.py,sha256=FPiEjghPbi-RVCOxODE_BRlkvtMsnrgbakhq7-YORuc,503
|
|
21
21
|
qv/core/config.py,sha256=0ARPWbLCV_RJhJZOVomldnuztxeLhkQPPkoiOrTO9Jw,3792
|
|
22
22
|
qv/core/context.py,sha256=kAI_QENd6c9kRiIE5Ejt2Um4MZsQ3TL9ubkl3w5AJF0,2575
|
|
23
|
-
qv/core/engine.py,sha256=
|
|
23
|
+
qv/core/engine.py,sha256=xYpM4JKHKE5s1npD__R2Fm7JxAZ203AazMcF91hXEt4,4675
|
|
24
24
|
qv/core/models.py,sha256=aQQTmyaBJsdof7oh8Onu2C2DLqQA6YbpLxQ0zB5utXw,4096
|
|
25
25
|
qv/core/project.py,sha256=pHpDbjhYs_8iZiIluXUcj_zwTZPwbKHyXDIWfTxgCFM,18218
|
|
26
|
-
qv/frameworks/__init__.py,sha256=
|
|
26
|
+
qv/frameworks/__init__.py,sha256=VRsvKo42xj0OAz4e-bkIZC3fp-MW7NYmeMdUQKK_WZA,459
|
|
27
|
+
qv/frameworks/base.py,sha256=4ue1HyXWXEtPF04u_AmUsrBY3xNJNlE6CQzXjUD8Mpc,691
|
|
28
|
+
qv/frameworks/fastapi.py,sha256=uhn8F0DUdi02Y073oSyltHvkVC3JbRp6Q4HrbMW5cXU,118523
|
|
29
|
+
qv/frameworks/sqlalchemy.py,sha256=aZF7yYgdBzeqrrmurzT-5X-K311OypFBuZyHDl61cHM,78946
|
|
27
30
|
qv/integrations/__init__.py,sha256=xBWG_BBOAn4l43MnjXmhbu4qjiMdYq32pVjmDO0Q8iM,37
|
|
28
31
|
qv/remediation/__init__.py,sha256=oHIxQraTgQVQUZ_kIorPR4t-YDVe7ygpn_dqXq_dmIU,252
|
|
29
32
|
qv/remediation/engine.py,sha256=QkmrHqAyV7Zf1Z0OtWvxySJlzjUxKAmz63bJPiWHlsE,12741
|
|
@@ -35,12 +38,13 @@ qv/reporters/html_reporter.py,sha256=WZHYu_ND1GVNUHjgUK4ts97KPp1DqbkoYp3_-eOgnoU
|
|
|
35
38
|
qv/reporters/json_reporter.py,sha256=RRzEZAMpOw8HGEVbYV8n_PCJ0PrIvFlOgAGptUxvtvY,446
|
|
36
39
|
qv/reporters/sarif.py,sha256=CZEtjmqB7sNCc4qZJBLtm_7cT_gYa8frI1jl9JHzuyU,3062
|
|
37
40
|
qv/reporters/terminal.py,sha256=H5c5-HMKSZ2PIHx-ZE4XA5bhMH826JHzBav0EKFqU44,4549
|
|
38
|
-
qv/rules/registry.py,sha256=
|
|
41
|
+
qv/rules/registry.py,sha256=8Uru3E-ljz3Z_rzfczUO1g5mb9OHY2ax665d9RoonQw,50143
|
|
39
42
|
qv/tui/__init__.py,sha256=otEuHr1bZ6rRMF2WkgPmv6z2Ws1cmnvFqr9Semhe_6A,100
|
|
40
43
|
qv/tui/app.py,sha256=Db08g5SMYsLZZ4ncrh_9ElIw_TyiRoh5gDiGkoF6mPI,11841
|
|
41
44
|
qv/visualizers/__init__.py,sha256=0kMeoXfdUJalMeZms5EwrFEYMpXmx30dJxITr9ahLCU,132
|
|
42
45
|
qv/visualizers/tree.py,sha256=Jf7w4HZzvxvmCIiGea1PzaUoCKN2ByAVdM8vXLHId-c,8043
|
|
43
|
-
python_qv-0.1.
|
|
44
|
-
python_qv-0.1.
|
|
45
|
-
python_qv-0.1.
|
|
46
|
-
python_qv-0.1.
|
|
46
|
+
python_qv-0.1.4.dist-info/METADATA,sha256=Ym3L7sA8JMuq5KCOVdA1oLe96DcEGOpPiLdBelT6sko,17206
|
|
47
|
+
python_qv-0.1.4.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
48
|
+
python_qv-0.1.4.dist-info/entry_points.txt,sha256=y0aZqfDPndkJ2yXxBM2RLL1qriH72zWEKSj4nJtc2q4,94
|
|
49
|
+
python_qv-0.1.4.dist-info/licenses/LICENSE,sha256=fNrhekSMdWyK_2XdiJjrAYoeGfyLdNMZ9hgW65gWzYs,1075
|
|
50
|
+
python_qv-0.1.4.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Inzamul Hoque
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
qv/__init__.py
CHANGED
qv/cli/main.py
CHANGED
|
@@ -18,6 +18,7 @@ from qv.core.config import QvConfig
|
|
|
18
18
|
from qv.core.engine import AnalysisEngine
|
|
19
19
|
from qv.core.models import Severity
|
|
20
20
|
from qv.core.project import ProjectDiscovery
|
|
21
|
+
from qv.frameworks import AVAILABLE_FRAMEWORK_ANALYZERS
|
|
21
22
|
from qv.remediation.engine import RemediationEngine
|
|
22
23
|
from qv.reporters.github_annotator import GitHubAnnotator
|
|
23
24
|
from qv.reporters.html_reporter import HtmlReporter
|
|
@@ -527,6 +528,89 @@ def graph_cmd(
|
|
|
527
528
|
)
|
|
528
529
|
|
|
529
530
|
|
|
531
|
+
@cli.command("framework")
|
|
532
|
+
@click.argument(
|
|
533
|
+
"path",
|
|
534
|
+
default=".",
|
|
535
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
|
|
536
|
+
)
|
|
537
|
+
@click.option(
|
|
538
|
+
"--name",
|
|
539
|
+
"-n",
|
|
540
|
+
type=str,
|
|
541
|
+
default=None,
|
|
542
|
+
help="Filter by framework plugin name (e.g. 'fastapi', 'sqlalchemy', 'sql').",
|
|
543
|
+
)
|
|
544
|
+
@click.option("--json", "as_json", is_flag=True, help="Output JSON results.")
|
|
545
|
+
@click.option("--sarif", "as_sarif", is_flag=True, help="Output SARIF results.")
|
|
546
|
+
def framework_cmd(path: Path, name: str | None, as_json: bool, as_sarif: bool) -> None:
|
|
547
|
+
"""Run framework-specific doctors (FastAPI, SQLAlchemy, Celery, Django)."""
|
|
548
|
+
try:
|
|
549
|
+
pyproject_path = path / "pyproject.toml"
|
|
550
|
+
config = QvConfig.from_pyproject(pyproject_path if pyproject_path.exists() else None)
|
|
551
|
+
discovery = ProjectDiscovery(root=path, config=config)
|
|
552
|
+
context = discovery.discover_context()
|
|
553
|
+
|
|
554
|
+
# Select framework analyzers
|
|
555
|
+
analyzers_to_run = []
|
|
556
|
+
for cls in AVAILABLE_FRAMEWORK_ANALYZERS:
|
|
557
|
+
inst = cls()
|
|
558
|
+
if name:
|
|
559
|
+
target_name = name.lower()
|
|
560
|
+
if target_name in (inst.id.lower(), inst.name.lower()) or (
|
|
561
|
+
target_name == "sql" and inst.id == "sqlalchemy"
|
|
562
|
+
):
|
|
563
|
+
analyzers_to_run.append(inst)
|
|
564
|
+
else:
|
|
565
|
+
analyzers_to_run.append(inst)
|
|
566
|
+
|
|
567
|
+
if not analyzers_to_run:
|
|
568
|
+
console.print(
|
|
569
|
+
f"[bold red]Unknown framework '{name}'. Available: fastapi, sqlalchemy[/bold red]"
|
|
570
|
+
)
|
|
571
|
+
sys.exit(2)
|
|
572
|
+
|
|
573
|
+
engine = AnalysisEngine(config=config, analyzers=analyzers_to_run)
|
|
574
|
+
result = engine.run(context)
|
|
575
|
+
|
|
576
|
+
if as_json:
|
|
577
|
+
reporter = JsonReporter()
|
|
578
|
+
click.echo(reporter.render(result))
|
|
579
|
+
elif as_sarif:
|
|
580
|
+
reporter = SarifReporter()
|
|
581
|
+
click.echo(reporter.render(result))
|
|
582
|
+
else:
|
|
583
|
+
TerminalReporter(console=console).print_result(result)
|
|
584
|
+
|
|
585
|
+
if result.has_blocking_errors:
|
|
586
|
+
sys.exit(1)
|
|
587
|
+
sys.exit(0)
|
|
588
|
+
except click.ClickException:
|
|
589
|
+
raise
|
|
590
|
+
except SystemExit:
|
|
591
|
+
raise
|
|
592
|
+
except Exception as e:
|
|
593
|
+
console.print(f"[bold red]Error running framework analysis: {e}[/bold red]")
|
|
594
|
+
sys.exit(3)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
@cli.command("frameworks")
|
|
598
|
+
@click.argument(
|
|
599
|
+
"path",
|
|
600
|
+
default=".",
|
|
601
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
|
|
602
|
+
)
|
|
603
|
+
@click.option("--name", "-n", type=str, default=None, help="Filter by framework plugin name.")
|
|
604
|
+
@click.option("--json", "as_json", is_flag=True, help="Output JSON results.")
|
|
605
|
+
@click.option("--sarif", "as_sarif", is_flag=True, help="Output SARIF results.")
|
|
606
|
+
@click.pass_context
|
|
607
|
+
def frameworks_cmd(
|
|
608
|
+
ctx: click.Context, path: Path, name: str | None, as_json: bool, as_sarif: bool
|
|
609
|
+
) -> None:
|
|
610
|
+
"""Alias for 'qv framework' command."""
|
|
611
|
+
ctx.invoke(framework_cmd, path=path, name=name, as_json=as_json, as_sarif=as_sarif)
|
|
612
|
+
|
|
613
|
+
|
|
530
614
|
@cli.command("inspect")
|
|
531
615
|
@click.argument(
|
|
532
616
|
"path",
|
qv/core/engine.py
CHANGED
|
@@ -13,6 +13,8 @@ from qv.core.analyzer import Analyzer
|
|
|
13
13
|
from qv.core.config import QvConfig
|
|
14
14
|
from qv.core.context import ProjectContext
|
|
15
15
|
from qv.core.models import Diagnostic, RootCause, ScanResult, Suggestion
|
|
16
|
+
from qv.frameworks.fastapi import FastApiAnalyzer
|
|
17
|
+
from qv.frameworks.sqlalchemy import SqlAlchemyAnalyzer
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class AnalysisEngine:
|
|
@@ -33,6 +35,8 @@ class AnalysisEngine:
|
|
|
33
35
|
DependencyAnalyzer(),
|
|
34
36
|
ImportAnalyzer(),
|
|
35
37
|
SecurityAnalyzer(),
|
|
38
|
+
FastApiAnalyzer(),
|
|
39
|
+
SqlAlchemyAnalyzer(),
|
|
36
40
|
]
|
|
37
41
|
|
|
38
42
|
def run(self, context: ProjectContext) -> ScanResult:
|
qv/frameworks/__init__.py
CHANGED
|
@@ -1 +1,19 @@
|
|
|
1
|
-
"""Framework
|
|
1
|
+
"""Framework analyzers for qv."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from qv.frameworks.base import FrameworkPlugin
|
|
6
|
+
from qv.frameworks.fastapi import FastApiAnalyzer
|
|
7
|
+
from qv.frameworks.sqlalchemy import SqlAlchemyAnalyzer
|
|
8
|
+
|
|
9
|
+
AVAILABLE_FRAMEWORK_ANALYZERS: list[type[FrameworkPlugin]] = [
|
|
10
|
+
FastApiAnalyzer,
|
|
11
|
+
SqlAlchemyAnalyzer,
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"FrameworkPlugin",
|
|
16
|
+
"FastApiAnalyzer",
|
|
17
|
+
"SqlAlchemyAnalyzer",
|
|
18
|
+
"AVAILABLE_FRAMEWORK_ANALYZERS",
|
|
19
|
+
]
|
qv/frameworks/base.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Base protocol and registry for framework-specific analyzers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
from qv.core.context import ProjectContext
|
|
8
|
+
from qv.core.models import Diagnostic
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@runtime_checkable
|
|
12
|
+
class FrameworkPlugin(Protocol):
|
|
13
|
+
"""Protocol for framework-specific diagnostic plugins."""
|
|
14
|
+
|
|
15
|
+
id: str
|
|
16
|
+
name: str
|
|
17
|
+
description: str
|
|
18
|
+
|
|
19
|
+
def detect(self, context: ProjectContext) -> bool:
|
|
20
|
+
"""Detect if this framework is actively used in the project."""
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
def analyze(self, context: ProjectContext) -> list[Diagnostic]:
|
|
24
|
+
"""Analyze the project context for framework-specific issues."""
|
|
25
|
+
...
|