python-qv 0.1.2__py3-none-any.whl → 0.1.3__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.3.dist-info}/METADATA +9 -2
- {python_qv-0.1.2.dist-info → python_qv-0.1.3.dist-info}/RECORD +12 -9
- 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.3.dist-info}/WHEEL +0 -0
- {python_qv-0.1.2.dist-info → python_qv-0.1.3.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.3
|
|
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
|
|
@@ -199,8 +199,10 @@ Run `qv inspect` (or `qv ui`) for a full terminal dashboard:
|
|
|
199
199
|
| | `IMP-002` | Unresolved relative or internal module imports | `ERROR` |
|
|
200
200
|
| **Packaging** | `PKG-001` | Missing PEP 621 metadata (name, version, etc.) | `WARNING` |
|
|
201
201
|
| | `PKG-002` | Invalid syntax or malformed keys in `pyproject.toml` | `ERROR` |
|
|
202
|
+
| **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` |
|
|
203
|
+
| **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
204
|
|
|
203
|
-
👉 *See
|
|
205
|
+
👉 *See all 80+ rules and remediation steps in the [Rules Catalog](docs/rules.md).*
|
|
204
206
|
|
|
205
207
|
---
|
|
206
208
|
|
|
@@ -331,6 +333,11 @@ qv environment
|
|
|
331
333
|
|
|
332
334
|
# Check AST & imports only (circular import loops, unresolvable modules)
|
|
333
335
|
qv architecture
|
|
336
|
+
|
|
337
|
+
# Check framework-specific issues (FastAPI, SQLAlchemy, SQLModel)
|
|
338
|
+
qv framework
|
|
339
|
+
qv framework --name fastapi
|
|
340
|
+
qv framework --name sqlalchemy
|
|
334
341
|
```
|
|
335
342
|
|
|
336
343
|
---
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
qv/__init__.py,sha256=
|
|
1
|
+
qv/__init__.py,sha256=LVdkSKnGrwgeYWTqy3QqNeA3X94yGLXt7-HAb2uxkZI,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,12 @@ 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.3.dist-info/METADATA,sha256=dOPYnxRmSexQjQBWWZJdPW3pynbr38uvxm49slMsCoY,16950
|
|
47
|
+
python_qv-0.1.3.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
48
|
+
python_qv-0.1.3.dist-info/entry_points.txt,sha256=y0aZqfDPndkJ2yXxBM2RLL1qriH72zWEKSj4nJtc2q4,94
|
|
49
|
+
python_qv-0.1.3.dist-info/RECORD,,
|
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
|
+
...
|