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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-qv
3
- Version: 0.1.2
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 full explanations and remediation steps in the [Rules Catalog](docs/rules.md).*
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=ytQrd73EtfTFFFPArsLrgOJNI9NNyUwp_B8_rlbd-Fg,100
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=aiDn3sjPVijPrNrW5YqyFBsO2KEL5h-V4rXhnNzzNP0,18806
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=foFddOmcSFuLWqmqYdcXWC-6vh_27IsgUF2l7XJrBJ4,4496
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=5XnOOPd2Kyb9uG3Z7MjqZO1emDZs5F1sCAmI4yjWMPI,25
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=sz-S59_AAE0dzTe4wb77q2xymwQra0mXmEDrXf-FuMo,8596
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.2.dist-info/METADATA,sha256=P7c1fy7JlOGs60A338evg0PvW6EkUwBxhJAcwW_cuPU,16415
44
- python_qv-0.1.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
45
- python_qv-0.1.2.dist-info/entry_points.txt,sha256=y0aZqfDPndkJ2yXxBM2RLL1qriH72zWEKSj4nJtc2q4,94
46
- python_qv-0.1.2.dist-info/RECORD,,
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
@@ -1,3 +1,3 @@
1
1
  """qv - Package-manager-agnostic diagnostic platform for Python projects."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.3"
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 plugins."""
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
+ ...