python-qv 0.1.0__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.
@@ -0,0 +1,126 @@
1
+ """Terminal reporter using Rich for structured, human-readable diagnostics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.text import Text
10
+
11
+ from qv.core.models import ScanResult, Severity
12
+
13
+
14
+ class TerminalReporter:
15
+ """Renders diagnostics to rich terminal output."""
16
+
17
+ def __init__(self, console: Console | None = None) -> None:
18
+ self.console = console or Console(color_system="auto")
19
+
20
+ def render(self, result: ScanResult) -> str:
21
+ """Render scan result to string for display."""
22
+ string_io = io.StringIO()
23
+ out_console = Console(file=string_io, force_terminal=True, width=90)
24
+ self.print_result(result, console=out_console)
25
+ return string_io.getvalue()
26
+
27
+ def print_result(self, result: ScanResult, console: Console | None = None) -> None:
28
+ """Print the scan result directly to console."""
29
+ con = console or self.console
30
+
31
+ # Header
32
+ con.print()
33
+ con.print("[bold cyan]🔍 qv[/bold cyan]")
34
+ con.print(f"[dim]Project:[/dim] [bold]{result.project_name}[/bold]")
35
+ con.print(f"[dim]Python:[/dim] [green]{result.python_version}[/green]")
36
+ con.print(f"[dim]Package Manager:[/dim] [blue]{result.package_manager}[/blue]")
37
+ con.print()
38
+
39
+ # Summary Badges
40
+ summary = result.summary
41
+ err_badge = (
42
+ f"[bold red]🔴 {summary.errors_count} Errors[/bold red]"
43
+ if summary.errors_count
44
+ else "[dim]0 Errors[/dim]"
45
+ )
46
+ warn_badge = (
47
+ f"[bold yellow]🟡 {summary.warnings_count} Warnings[/bold yellow]"
48
+ if summary.warnings_count
49
+ else "[dim]0 Warnings[/dim]"
50
+ )
51
+ pass_badge = f"[bold green]đŸŸĸ {summary.checks_passed} Checks Passed[/bold green]"
52
+
53
+ con.print(f"{err_badge} {warn_badge} {pass_badge}")
54
+ con.print()
55
+
56
+ # Diagnostics listing
57
+ if not result.diagnostics:
58
+ con.print(
59
+ "[bold green]✨ Everything looks healthy! No diagnostic issues found.[/bold green]"
60
+ )
61
+ else:
62
+ for diag in result.diagnostics:
63
+ color = (
64
+ "red"
65
+ if diag.severity == Severity.ERROR
66
+ else "yellow"
67
+ if diag.severity == Severity.WARNING
68
+ else "blue"
69
+ )
70
+ icon = (
71
+ "🔴"
72
+ if diag.severity == Severity.ERROR
73
+ else "🟡"
74
+ if diag.severity == Severity.WARNING
75
+ else "â„šī¸"
76
+ )
77
+
78
+ title_text = Text()
79
+ title_text.append(f"{icon} {diag.id} ", style=f"bold {color}")
80
+ title_text.append(f"{diag.title}", style="bold white")
81
+
82
+ body = []
83
+ body.append(f"[bold]{diag.message}[/bold]")
84
+
85
+ if diag.file:
86
+ loc = f"[dim]Location:[/dim] {diag.file}"
87
+ if diag.line:
88
+ loc += f":{diag.line}"
89
+ body.append(loc)
90
+
91
+ if diag.evidence:
92
+ body.append("\n[bold dim]Evidence:[/bold dim]")
93
+ for ev in diag.evidence:
94
+ body.append(f" â€ĸ {ev.fact}")
95
+
96
+ if diag.suggestions:
97
+ body.append("\n[bold cyan]Suggested fix:[/bold cyan]")
98
+ for sug in diag.suggestions:
99
+ body.append(f" {sug.description}")
100
+ if sug.command:
101
+ body.append(f" [bold green]$ {sug.command}[/bold green]")
102
+ if sug.code_snippet:
103
+ body.append(
104
+ f" [dim]```[/dim]\n [white]{sug.code_snippet}[/white]\n [dim]```[/dim]"
105
+ )
106
+
107
+ panel = Panel(
108
+ "\n".join(body),
109
+ title=title_text,
110
+ border_style=color,
111
+ padding=(0, 1),
112
+ )
113
+ con.print(panel)
114
+
115
+ # Health score footer
116
+ score_color = (
117
+ "green"
118
+ if summary.health_score >= 85
119
+ else "yellow"
120
+ if summary.health_score >= 60
121
+ else "red"
122
+ )
123
+ con.print(
124
+ f"[bold]Health Score:[/bold] [{score_color}]{summary.health_score}/100[/{score_color}]"
125
+ )
126
+ con.print()
qv/rules/registry.py ADDED
@@ -0,0 +1,151 @@
1
+ """Central registry of all supported diagnostic rules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from qv.core.models import Severity
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class RuleDefinition:
12
+ """Metadata for a diagnostic rule."""
13
+
14
+ id: str
15
+ category: str
16
+ title: str
17
+ description: str
18
+ default_severity: Severity
19
+ remediation_hint: str
20
+ doc_url: str | None = None
21
+
22
+
23
+ # Stable Registry of rules as defined in docs/rules.md
24
+ RULES_CATALOG: dict[str, RuleDefinition] = {
25
+ # Dependency rules
26
+ "DEP-001": RuleDefinition(
27
+ id="DEP-001",
28
+ category="dependency",
29
+ title="Dependency constraint conflict",
30
+ description="Two or more declared or transitive dependencies have incompatible version constraints.",
31
+ default_severity=Severity.ERROR,
32
+ remediation_hint="Upgrade the conflicting package or loosen the version pin to satisfy all requirements.",
33
+ doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#dep-001--dependency-constraint-conflict",
34
+ ),
35
+ "DEP-002": RuleDefinition(
36
+ id="DEP-002",
37
+ category="dependency",
38
+ title="Missing dependency declaration",
39
+ description="A third-party package is imported in source code but is not declared in project dependencies.",
40
+ default_severity=Severity.ERROR,
41
+ remediation_hint="Add the missing package to your pyproject.toml or requirements.txt.",
42
+ doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#dep-002--missing-dependency-declaration",
43
+ ),
44
+ "DEP-003": RuleDefinition(
45
+ id="DEP-003",
46
+ category="dependency",
47
+ title="Unused declared dependency",
48
+ description="A package is declared as a direct dependency but no imports were detected across project source files.",
49
+ default_severity=Severity.WARNING,
50
+ remediation_hint="Verify if this dependency is needed at runtime or remove it to keep dependencies lean.",
51
+ doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#dep-003--unused-declared-dependency",
52
+ ),
53
+ "DEP-004": RuleDefinition(
54
+ id="DEP-004",
55
+ category="dependency",
56
+ title="Python compatibility mismatch",
57
+ description="A package requires a Python version incompatible with the target project runtime.",
58
+ default_severity=Severity.WARNING,
59
+ remediation_hint="Update your target Python version or install a version of the package compatible with your Python runtime.",
60
+ doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#dep-004--python-compatibility-mismatch",
61
+ ),
62
+ "DEP-005": RuleDefinition(
63
+ id="DEP-005",
64
+ category="dependency",
65
+ title="Installed/declaration mismatch",
66
+ description="The package version installed in the virtualenv does not match the pinned requirement in project declaration.",
67
+ default_severity=Severity.WARNING,
68
+ remediation_hint="Sync your virtual environment using your package manager (e.g. `uv sync` or `poetry install`).",
69
+ doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#dep-005--installeddeclaration-mismatch",
70
+ ),
71
+ "DEP-006": RuleDefinition(
72
+ id="DEP-006",
73
+ category="dependency",
74
+ title="Vulnerable transitive dependency",
75
+ description="A security vulnerability has been identified in a direct or transitive dependency.",
76
+ default_severity=Severity.ERROR,
77
+ remediation_hint="Update the vulnerable dependency or apply vendor security patches.",
78
+ doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#dep-006--vulnerable-transitive-dependency",
79
+ ),
80
+ # Environment rules
81
+ "ENV-001": RuleDefinition(
82
+ id="ENV-001",
83
+ category="environment",
84
+ title="Python version drift",
85
+ description="The active Python interpreter version differs from the project's target runtime specification.",
86
+ default_severity=Severity.WARNING,
87
+ remediation_hint="Ensure the virtual environment is built using the configured target Python version.",
88
+ doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#env-001--python-version-drift",
89
+ ),
90
+ "ENV-002": RuleDefinition(
91
+ id="ENV-002",
92
+ category="environment",
93
+ title="Docker runtime drift",
94
+ description="The Python version in Dockerfile/docker-compose differs from the project's target runtime.",
95
+ default_severity=Severity.WARNING,
96
+ remediation_hint="Update the base image tag in your Dockerfile to match your project's target Python version.",
97
+ doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#env-002--docker-runtime-drift",
98
+ ),
99
+ "ENV-003": RuleDefinition(
100
+ id="ENV-003",
101
+ category="environment",
102
+ title="CI runtime drift",
103
+ description="CI workflow test matrix does not cover or differs from declared project Python versions.",
104
+ default_severity=Severity.WARNING,
105
+ remediation_hint="Align your CI matrix with the Python versions specified in pyproject.toml.",
106
+ doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#env-003--ci-runtime-drift",
107
+ ),
108
+ # Import / Architecture rules
109
+ "IMP-001": RuleDefinition(
110
+ id="IMP-001",
111
+ category="architecture",
112
+ title="Circular import detected",
113
+ description="An import cycle exists between two or more modules, which may cause runtime initialization errors.",
114
+ default_severity=Severity.ERROR,
115
+ remediation_hint="Refactor shared dependencies into a separate module or use deferred/lazy imports.",
116
+ doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#imp-001--circular-import-detected",
117
+ ),
118
+ "IMP-002": RuleDefinition(
119
+ id="IMP-002",
120
+ category="architecture",
121
+ title="Unresolved local import",
122
+ description="A local module imported in source code could not be resolved on the project's Python path.",
123
+ default_severity=Severity.ERROR,
124
+ remediation_hint="Verify the module name and ensure source directory is marked on the PYTHONPATH or package root.",
125
+ doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#imp-002--unresolved-local-import",
126
+ ),
127
+ # Packaging rules
128
+ "PKG-001": RuleDefinition(
129
+ id="PKG-001",
130
+ category="packaging",
131
+ title="Missing package metadata",
132
+ description="Essential packaging metadata (such as project name, version, or description) is missing.",
133
+ default_severity=Severity.WARNING,
134
+ remediation_hint="Provide project name and version in pyproject.toml [project] table.",
135
+ doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#pkg-001--missing-package-metadata",
136
+ ),
137
+ "PKG-002": RuleDefinition(
138
+ id="PKG-002",
139
+ category="packaging",
140
+ title="Invalid project configuration",
141
+ description="The project configuration file contains syntax errors or invalid keys.",
142
+ default_severity=Severity.ERROR,
143
+ remediation_hint="Fix configuration syntax to conform to PEP 621 / tool specifications.",
144
+ doc_url="https://github.com/inzamol/qv/blob/main/docs/rules.md#pkg-002--invalid-project-configuration",
145
+ ),
146
+ }
147
+
148
+
149
+ def get_rule_definition(rule_id: str) -> RuleDefinition | None:
150
+ """Retrieve rule definition by ID."""
151
+ return RULES_CATALOG.get(rule_id.upper())