@vasanth-mv/pqs-cli 1.1.0

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.
Binary file
@@ -0,0 +1,236 @@
1
+ # qcBot — Python package
2
+
3
+ > **Python interface for the qcBot test quality analyser.**
4
+ > The rule engine runs in **Node.js** (via `npx @cqs/qcbot`).
5
+ > Node.js 18+ must be on your PATH — the package checks for it at call time.
6
+
7
+ ---
8
+
9
+ ## Why Python?
10
+
11
+ The rule engine is written in JavaScript/TypeScript and runs in Node.js.
12
+ This Python package is a **thin subprocess bridge** — the same pattern
13
+ Playwright itself uses for its Python SDK.
14
+
15
+ - Zero rule duplication — one engine, many language wrappers
16
+ - Returns structured Python objects (`PwQualityResult`, `FileResult`, `Finding`)
17
+ - CLI (`qcbot-py check`) produces the same colour output as the Node.js CLI
18
+ - Works in Python 3.9+
19
+
20
+ ---
21
+
22
+ ## Requirements
23
+
24
+ | Requirement | Version |
25
+ |-------------|---------|
26
+ | Python | 3.9+ |
27
+ | Node.js + npx | **18+** (checked at runtime) |
28
+
29
+ ---
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ # Library only (use as import)
35
+ pip install qcbot
36
+
37
+ # Library + command-line tool
38
+ pip install "qcbot[cli]"
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Quickstart — CLI
44
+
45
+ ```bash
46
+ # Analyse a folder, fail if score < 80, generate HTML report
47
+ qcbot-py check ./functional-tests --threshold 80 --report
48
+
49
+ # List all supported stacks
50
+ qcbot-py stacks
51
+
52
+ # Get JSON output (pipe into jq, scripts, etc.)
53
+ qcbot-py check ./tests --json | python3 -c "import sys,json; r=json.load(sys.stdin); print(r['score'], r['passed'])"
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Quickstart — Python API
59
+
60
+ ```python
61
+ from pw_quality import check
62
+
63
+ # Basic check
64
+ result = check("./functional-tests", threshold=80)
65
+
66
+ print(result.score) # 87
67
+ print(result.passed) # True
68
+ print(result.grade) # "Good"
69
+ print(result.summary) # Summary(critical=12, warnings=34, total=284, files=168)
70
+
71
+ # Per-file breakdown
72
+ for file in result.files:
73
+ print(f"{file.score:3} {file.name} ({file.critical_count} critical)")
74
+
75
+ # All findings
76
+ for finding in result.findings:
77
+ print(f"[{finding.severity}] {finding.title} — {finding.file}:{finding.line}")
78
+
79
+ # Critical findings only
80
+ criticals = result.critical_findings
81
+ ```
82
+
83
+ ---
84
+
85
+ ## API Reference
86
+
87
+ ### `check(path, *, stack, threshold, ...) → PwQualityResult`
88
+
89
+ | Parameter | Type | Default | Description |
90
+ |-----------|------|---------|-------------|
91
+ | `path` | str / Path | — | Directory with test files |
92
+ | `stack` | str | `"playwright"` | Rule set id (`"typescript"`, `"pytest_api"`, …) |
93
+ | `threshold` | int | `80` | Minimum passing score |
94
+ | `parallel` | int | `4` | Parallel workers |
95
+ | `report` | bool | `False` | Write HTML report to disk |
96
+ | `output_dir` | str | `None` | Override report output directory |
97
+ | `no_cross_file` | bool | `False` | Skip cross-file duplicate detection |
98
+ | `project` | str | `None` | Project name in HTML report |
99
+ | `npm_package` | str | `"@cqs/qcbot"` | Override for private npm registry |
100
+ | `verbose` | bool | `False` | Print raw CLI output to stdout |
101
+
102
+ ### `PwQualityResult`
103
+
104
+ | Attribute | Type | Description |
105
+ |-----------|------|-------------|
106
+ | `score` | int | Average quality score 0–100 |
107
+ | `passed` | bool | True if score ≥ threshold |
108
+ | `grade` | str | "Excellent" / "Good" / "Fair" / "Needs work" |
109
+ | `threshold` | int | Threshold used for this run |
110
+ | `summary` | Summary | `.critical`, `.warnings`, `.total`, `.files` |
111
+ | `files` | list[FileResult] | Per-file breakdown |
112
+ | `findings` | list[Finding] | All findings (flat, all files) |
113
+ | `critical_findings` | list[Finding] | Only severity=critical |
114
+ | `report_path` | str or None | Path to HTML report if `report=True` |
115
+
116
+ ---
117
+
118
+ ## CI / CD Integration
119
+
120
+ ### GitHub Actions
121
+
122
+ ```yaml
123
+ - name: Install qcbot
124
+ run: pip install "qcbot[cli]"
125
+
126
+ - name: Quality Gate
127
+ run: qcbot-py check ./tests --threshold 80 --report
128
+
129
+ - name: Upload report
130
+ if: always()
131
+ uses: actions/upload-artifact@v4
132
+ with:
133
+ name: quality-report
134
+ path: qcbot-report/report.html
135
+ ```
136
+
137
+ ### pytest integration
138
+
139
+ ```python
140
+ # conftest.py — add quality gate as a pytest fixture / session hook
141
+ import pytest
142
+ from pw_quality import check, PwQualityError
143
+
144
+ def pytest_sessionfinish(session, exitstatus):
145
+ try:
146
+ result = check("./tests", threshold=80)
147
+ if not result.passed:
148
+ print(f"\n⚠ Quality gate FAILED — score {result.score} < 80")
149
+ except PwQualityError as e:
150
+ print(f"\n⚠ qcBot not available: {e}")
151
+ ```
152
+
153
+ ### Django / Flask API endpoint
154
+
155
+ ```python
156
+ from pw_quality import check, PwQualityResult
157
+
158
+ @app.post("/api/quality-gate")
159
+ def quality_gate():
160
+ data = request.json
161
+ result: PwQualityResult = check(
162
+ data["path"],
163
+ stack=data.get("stack", "playwright"),
164
+ threshold=data.get("threshold", 80),
165
+ )
166
+ return {
167
+ "score": result.score,
168
+ "passed": result.passed,
169
+ "grade": result.grade,
170
+ "critical": result.summary.critical,
171
+ "findings": [
172
+ {"rule": f.rule_id, "severity": f.severity, "title": f.title, "line": f.line}
173
+ for f in result.findings
174
+ ],
175
+ }
176
+ ```
177
+
178
+ ### Jenkins (Python-based pipeline)
179
+
180
+ ```python
181
+ # Jenkinsfile (python-pipeline plugin) or a build script
182
+ from pw_quality import check
183
+
184
+ result = check("./functional-tests", threshold=80, report=True)
185
+ print(f"Quality score: {result.score} — {'PASSED' if result.passed else 'FAILED'}")
186
+ if not result.passed:
187
+ raise SystemExit(1) # Fails the Jenkins step
188
+ ```
189
+
190
+ ---
191
+
192
+ ## Supported stacks
193
+
194
+ ```bash
195
+ qcbot-py stacks
196
+ ```
197
+
198
+ Includes: `playwright`, `typescript`, `java_api`, `restassured`, `karate`,
199
+ `pytest_api`, `postman`, `python_api`, `ts_frontend`, `python_frontend`,
200
+ `playwright_java`, `playwright_python`, and more.
201
+
202
+ ---
203
+
204
+ ## Error handling
205
+
206
+ ```python
207
+ from pw_quality import check, PwQualityError
208
+
209
+ try:
210
+ result = check("./tests")
211
+ except PwQualityError as e:
212
+ print(f"Analysis failed: {e}")
213
+ # Common causes:
214
+ # - Node.js not found → install Node 18+
215
+ # - npx not available → ships with Node; re-install
216
+ # - Unknown stack → run qcbot-py stacks
217
+ ```
218
+
219
+ ---
220
+
221
+ ## FAQ
222
+
223
+ **Q: Does it send my code anywhere?**
224
+ > No. All rule-based analysis runs locally in Node.js. Only if you pass `--llm` to the underlying CLI does it contact an AI provider, using your own API key.
225
+
226
+ **Q: Do I need to install `@cqs/qcbot` separately?**
227
+ > No — `npx` downloads and caches it automatically on first run.
228
+
229
+ **Q: Can I use a private npm registry?**
230
+ > Yes: `check(path, npm_package="@cqs/qcbot")` and set `NPM_TOKEN` in your environment.
231
+
232
+ ---
233
+
234
+ ## Licence
235
+
236
+ MIT — © Your Company
@@ -0,0 +1,21 @@
1
+ """
2
+ pw_quality — Python interface for the qcBot CLI.
3
+
4
+ The rule engine lives in Node.js (npm: @cqs/qcbot).
5
+ This package is a thin wrapper that:
6
+ 1. Checks Node.js ≥ 18 is available
7
+ 2. Optionally bootstraps the npm package via npx
8
+ 3. Calls the CLI as a subprocess and returns structured Python objects
9
+
10
+ Usage
11
+ -----
12
+ from pw_quality import check, stacks
13
+
14
+ result = check("./functional-tests", threshold=80, report=True)
15
+ print(result.score, result.passed, result.findings)
16
+ """
17
+
18
+ from .runner import check, stacks, PwQualityResult, PwQualityError
19
+
20
+ __all__ = ["check", "stacks", "PwQualityResult", "PwQualityError"]
21
+ __version__ = "1.0.0"
@@ -0,0 +1,177 @@
1
+ """
2
+ cli.py — Click-based command-line interface for the pw_quality Python package.
3
+
4
+ Installed as `qcbot-py` by pyproject.toml.
5
+ Thin shell over runner.py — all analysis runs in Node.js via npx.
6
+ """
7
+
8
+ import sys
9
+
10
+ try:
11
+ import click
12
+ except ImportError:
13
+ print(
14
+ "ERROR: 'click' is required. Install it with: pip install qcbot[cli]",
15
+ file=sys.stderr,
16
+ )
17
+ sys.exit(1)
18
+
19
+ from . import runner
20
+
21
+
22
+ # ── Formatting helpers ────────────────────────────────────────────────────────
23
+
24
+ def _grade_color(score: int) -> str:
25
+ if score >= 90: return "bright_green"
26
+ if score >= 75: return "cyan"
27
+ if score >= 60: return "yellow"
28
+ return "red"
29
+
30
+
31
+ def _sev_color(sev: str) -> str:
32
+ return {"critical": "red", "warning": "yellow"}.get(sev, "cyan")
33
+
34
+
35
+ def _bar(score: int, width: int = 20) -> str:
36
+ filled = round(score / 100 * width)
37
+ return "█" * filled + "░" * (width - filled)
38
+
39
+
40
+ # ── CLI definition ────────────────────────────────────────────────────────────
41
+
42
+ @click.group()
43
+ @click.version_option("1.0.0", prog_name="qcbot-py")
44
+ def cli():
45
+ """qcBot — Playwright & multi-stack test quality analyser (Python interface).\n
46
+ The rule engine runs in Node.js (via npx @cqs/qcbot).
47
+ Node.js 18+ must be available on your PATH.
48
+ """
49
+
50
+
51
+ @cli.command()
52
+ @click.argument("path")
53
+ @click.option("-s", "--stack", default="playwright", show_default=True, help="Rule set id. Run `qcbot-py stacks` for options.")
54
+ @click.option("-t", "--threshold", default=80, show_default=True, type=int, help="Fail if score is below this (0–100).")
55
+ @click.option("-p", "--parallel", default=4, show_default=True, type=int, help="Max files analysed in parallel.")
56
+ @click.option("--report", is_flag=True, help="Generate self-contained HTML report.")
57
+ @click.option("--output", default=None, help="Output directory for HTML/JSON reports.")
58
+ @click.option("--no-cross-file", is_flag=True, help="Skip cross-file duplicate detection.")
59
+ @click.option("--project", default=None, help="Project name shown in the HTML report header.")
60
+ @click.option("--json", "json_out",is_flag=True, help="Print the full JSON result to stdout instead of the summary table.")
61
+ @click.option("--package", default="@cqs/qcbot", help="Override npm package name (for private registries).")
62
+ def check(path, stack, threshold, parallel, report, output, no_cross_file, project, json_out, package):
63
+ """Analyse test files in PATH and report quality findings.
64
+
65
+ PATH is a directory or glob that contains the spec/test files.
66
+
67
+ \b
68
+ Examples:
69
+ qcbot-py check ./functional-tests
70
+ qcbot-py check ./tests --stack pytest_api --threshold 75 --report
71
+ qcbot-py check ./src/tests --json | jq '.score'
72
+ """
73
+ click.echo(f"\n⚡ qcBot · stack: {stack} · threshold: {threshold}\n")
74
+
75
+ try:
76
+ result = runner.check(
77
+ path,
78
+ stack=stack,
79
+ threshold=threshold,
80
+ parallel=parallel,
81
+ report=report,
82
+ output_dir=output,
83
+ no_cross_file=no_cross_file,
84
+ project=project,
85
+ npm_package=package,
86
+ verbose=False,
87
+ )
88
+ except runner.PwQualityError as exc:
89
+ click.secho(f"✗ Error: {exc}", fg="red", err=True)
90
+ sys.exit(2)
91
+
92
+ if json_out:
93
+ import json as _json, dataclasses
94
+ def _serial(obj):
95
+ if dataclasses.is_dataclass(obj):
96
+ return dataclasses.asdict(obj)
97
+ raise TypeError(f"Not serialisable: {type(obj)}")
98
+ click.echo(_json.dumps(dataclasses.asdict(result), indent=2, default=_serial))
99
+ sys.exit(0 if result.passed else 1)
100
+
101
+ # ── Pretty terminal output ────────────────────────────────────────────────
102
+ gc = _grade_color(result.score)
103
+ click.echo("Results")
104
+ click.echo("─" * 52)
105
+ click.secho(f" Score {_bar(result.score)} {result.score} {result.grade}", fg=gc, bold=True)
106
+ click.echo(f" Files {result.summary.files}")
107
+ crit_color = "red" if result.summary.critical > 0 else "green"
108
+ warn_color = "yellow" if result.summary.warnings > 0 else "green"
109
+ click.secho(f" Critical {result.summary.critical}", fg=crit_color, nl=False)
110
+ click.echo(" ", nl=False)
111
+ click.secho(f"Warnings {result.summary.warnings}", fg=warn_color, nl=False)
112
+ click.echo(f" Total {result.summary.total}")
113
+ status_color = "green" if result.passed else "red"
114
+ status_text = "✓ PASSED" if result.passed else "✗ FAILED"
115
+ click.secho(f" Status {status_text} (threshold {threshold})", fg=status_color, bold=True)
116
+ click.echo("─" * 52)
117
+
118
+ # Per-file
119
+ click.echo()
120
+ click.echo("File breakdown")
121
+ for fr in result.files:
122
+ fc = _grade_color(fr.score)
123
+ crit = fr.critical_count
124
+ crit_label = click.style(f"{crit} crit ", fg="red") if crit > 0 else click.style("✓ ", fg="green")
125
+ click.echo(
126
+ f" {click.style(str(fr.score).rjust(3), fg=fc)} "
127
+ f"{_bar(fr.score, 14)} "
128
+ f"{crit_label}"
129
+ f"{click.style(fr.name, fg='bright_black')}"
130
+ )
131
+
132
+ # Top findings
133
+ top = [f for f in result.findings if f.severity in ("critical", "warning")][:10]
134
+ if top:
135
+ click.echo()
136
+ click.echo("Top findings")
137
+ for f in top:
138
+ click.secho(f" {f.severity.upper():<9}", fg=_sev_color(f.severity), bold=True, nl=False)
139
+ click.echo(f" {f.title}")
140
+ loc = f.file or ""
141
+ if f.line:
142
+ loc += f":{f.line}"
143
+ click.secho(f" {loc} {f.rule_id}", fg="bright_black")
144
+
145
+ # Report link
146
+ if result.report_path:
147
+ click.echo()
148
+ click.secho(f"✓ HTML report → {result.report_path}", fg="green")
149
+ click.echo(f" Open: open {result.report_path}")
150
+
151
+ click.echo()
152
+ sys.exit(0 if result.passed else 1)
153
+
154
+
155
+ @cli.command()
156
+ @click.option("--package", default="@cqs/qcbot", help="Override npm package name.")
157
+ def stacks(package):
158
+ """List all supported stacks (language/framework rule sets)."""
159
+ try:
160
+ all_stacks = runner.stacks(npm_package=package)
161
+ except runner.PwQualityError as exc:
162
+ click.secho(f"✗ Error: {exc}", fg="red", err=True)
163
+ sys.exit(2)
164
+
165
+ click.echo("\nSupported stacks\n")
166
+ for s in all_stacks:
167
+ click.secho(f" {s['id']:<22}", fg="cyan", bold=True, nl=False)
168
+ click.echo(s.get("label", ""))
169
+ click.echo()
170
+
171
+
172
+ def main():
173
+ cli()
174
+
175
+
176
+ if __name__ == "__main__":
177
+ main()