Initial release — APIClient - Agent v2.0.0
AI-first API testing desktop client built with Python + PyQt6. Features: - Multi-tab HTTP request editor with params/headers/body/auth/tests - KeyValueTable with per-row enable/disable checkboxes and 36px rows - Format JSON button, syntax highlighting, pre-request & test scripts - Collections, environments, history, import/export (Postman v2.1, cURL) - OpenAPI 3.x / Swagger 2.0 local parser (no AI tokens) - EKIKA Odoo API Framework generator — JSON-API, REST JSON, GraphQL, Custom REST JSON with all auth types (instant, no AI tokens) - Persistent AI chat sidebar (Claude-powered co-pilot) with streaming, context-aware suggestions, and one-click Apply to request editor - AI collection generator from any docs URL or pasted spec - WebSocket client, Mock server, Collection runner, Code generator - Dark/light theme engine (global QSS, object-name selectors) - SSL error detection with actionable hints - MIT License Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
194
app/ui/collection_runner.py
Normal file
194
app/ui/collection_runner.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""APIClient - Agent — Collection Runner dialog."""
|
||||
from PyQt6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QTreeWidget, QTreeWidgetItem, QComboBox, QHeaderView, QProgressBar, QWidget
|
||||
)
|
||||
from PyQt6.QtCore import QThread, pyqtSignal, Qt
|
||||
from PyQt6.QtGui import QBrush, QColor
|
||||
|
||||
from app.ui.theme import Colors
|
||||
from app.core import storage, http_client
|
||||
from app.core.test_runner import run_tests
|
||||
from app.models import HttpRequest, CollectionRunResult
|
||||
|
||||
|
||||
class RunnerWorker(QThread):
|
||||
result_ready = pyqtSignal(object)
|
||||
finished = pyqtSignal()
|
||||
|
||||
def __init__(self, requests: list[dict], variables: dict):
|
||||
super().__init__()
|
||||
self.requests = requests
|
||||
self.variables = variables
|
||||
|
||||
def run(self):
|
||||
for r in self.requests:
|
||||
req = HttpRequest(
|
||||
method = r.get("method") or "GET",
|
||||
url = r.get("url") or "",
|
||||
headers = r.get("headers") or {},
|
||||
params = r.get("params") or {},
|
||||
body = r.get("body") or "",
|
||||
body_type = r.get("body_type") or "raw",
|
||||
auth_type = r.get("auth_type") or "none",
|
||||
auth_data = r.get("auth_data") or {},
|
||||
test_script = r.get("test_script") or "",
|
||||
name = r.get("name") or r.get("url", ""),
|
||||
timeout = r.get("timeout") or 30,
|
||||
ssl_verify = bool(r.get("ssl_verify", 1)),
|
||||
)
|
||||
resp = http_client.send_request(req, self.variables)
|
||||
test_results = run_tests(req.test_script, resp)
|
||||
result = CollectionRunResult(
|
||||
request_name = req.name or req.url,
|
||||
method = req.method,
|
||||
url = req.url,
|
||||
status = resp.status,
|
||||
elapsed_ms = resp.elapsed_ms,
|
||||
test_results = test_results,
|
||||
error = resp.error,
|
||||
)
|
||||
self.result_ready.emit(result)
|
||||
self.finished.emit()
|
||||
|
||||
|
||||
class CollectionRunnerDialog(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Collection Runner")
|
||||
self.setMinimumSize(800, 550)
|
||||
self._worker = None
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
# ── Header ────────────────────────────────────────────────────────────
|
||||
header = QWidget()
|
||||
header.setObjectName("panelHeader")
|
||||
header.setFixedHeight(52)
|
||||
hl = QHBoxLayout(header)
|
||||
hl.setContentsMargins(16, 0, 16, 0)
|
||||
title = QLabel("Collection Runner")
|
||||
title.setObjectName("panelTitle")
|
||||
hl.addWidget(title)
|
||||
hl.addStretch()
|
||||
col_label = QLabel("Collection:")
|
||||
col_label.setObjectName("fieldLabel")
|
||||
self.col_combo = QComboBox()
|
||||
self.col_combo.setMinimumWidth(200)
|
||||
self._collections = storage.get_collections()
|
||||
for c in self._collections:
|
||||
self.col_combo.addItem(c["name"], c["id"])
|
||||
self.run_btn = QPushButton("Run All")
|
||||
self.run_btn.setObjectName("accent")
|
||||
self.run_btn.setFixedWidth(100)
|
||||
self.run_btn.clicked.connect(self._run)
|
||||
hl.addWidget(col_label)
|
||||
hl.addWidget(self.col_combo)
|
||||
hl.addSpacing(8)
|
||||
hl.addWidget(self.run_btn)
|
||||
layout.addWidget(header)
|
||||
|
||||
# ── Body ──────────────────────────────────────────────────────────────
|
||||
body = QWidget()
|
||||
body.setObjectName("panelBody")
|
||||
bl = QVBoxLayout(body)
|
||||
bl.setContentsMargins(16, 12, 16, 12)
|
||||
|
||||
self.progress = QProgressBar()
|
||||
self.progress.setValue(0)
|
||||
bl.addWidget(self.progress)
|
||||
|
||||
self.result_tree = QTreeWidget()
|
||||
self.result_tree.setHeaderLabels(["Request", "Status", "Time", "Tests"])
|
||||
self.result_tree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
||||
self.result_tree.header().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
|
||||
self.result_tree.header().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
|
||||
self.result_tree.header().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
|
||||
bl.addWidget(self.result_tree)
|
||||
|
||||
self.summary_label = QLabel("")
|
||||
self.summary_label.setObjectName("fieldLabel")
|
||||
bl.addWidget(self.summary_label)
|
||||
|
||||
layout.addWidget(body, 1)
|
||||
|
||||
# ── Footer ────────────────────────────────────────────────────────────
|
||||
footer = QWidget()
|
||||
footer.setObjectName("panelFooter")
|
||||
footer.setFixedHeight(52)
|
||||
fl = QHBoxLayout(footer)
|
||||
fl.setContentsMargins(16, 0, 16, 0)
|
||||
fl.addStretch()
|
||||
close_btn = QPushButton("Close")
|
||||
close_btn.setFixedWidth(80)
|
||||
close_btn.clicked.connect(self.accept)
|
||||
fl.addWidget(close_btn)
|
||||
layout.addWidget(footer)
|
||||
|
||||
def _run(self):
|
||||
col_id = self.col_combo.currentData()
|
||||
if col_id is None:
|
||||
return
|
||||
requests = storage.get_all_requests(col_id)
|
||||
if not requests:
|
||||
self.summary_label.setText("No requests in this collection.")
|
||||
return
|
||||
|
||||
self.result_tree.clear()
|
||||
self.progress.setMaximum(len(requests))
|
||||
self.progress.setValue(0)
|
||||
self.run_btn.setEnabled(False)
|
||||
self._done = 0
|
||||
self._passed_tests = 0
|
||||
self._total_tests = 0
|
||||
|
||||
env = storage.get_active_environment()
|
||||
variables = env.variables if env else {}
|
||||
|
||||
self._worker = RunnerWorker(requests, variables)
|
||||
self._worker.result_ready.connect(self._on_result)
|
||||
self._worker.finished.connect(self._on_finished)
|
||||
self._worker.start()
|
||||
|
||||
def _on_result(self, result: CollectionRunResult):
|
||||
self._done += 1
|
||||
self.progress.setValue(self._done)
|
||||
|
||||
passed = sum(1 for t in result.test_results if t.passed)
|
||||
total = len(result.test_results)
|
||||
self._passed_tests += passed
|
||||
self._total_tests += total
|
||||
|
||||
if result.error:
|
||||
status_str = "Error"
|
||||
row_color = Colors.ERROR
|
||||
else:
|
||||
status_str = str(result.status)
|
||||
row_color = Colors.SUCCESS if result.status < 400 else Colors.ERROR
|
||||
|
||||
test_str = f"{passed}/{total}" if total > 0 else "—"
|
||||
item = QTreeWidgetItem([
|
||||
f"{result.method} {result.request_name}",
|
||||
status_str,
|
||||
f"{result.elapsed_ms:.0f} ms",
|
||||
test_str,
|
||||
])
|
||||
item.setForeground(1, QBrush(QColor(row_color)))
|
||||
self.result_tree.addTopLevelItem(item)
|
||||
|
||||
for tr in result.test_results:
|
||||
icon = "✓" if tr.passed else "✗"
|
||||
child = QTreeWidgetItem([f" {icon} {tr.name}", "", "", tr.message])
|
||||
child.setForeground(0, QBrush(QColor(Colors.SUCCESS if tr.passed else Colors.ERROR)))
|
||||
item.addChild(child)
|
||||
|
||||
item.setExpanded(True)
|
||||
|
||||
def _on_finished(self):
|
||||
self.run_btn.setEnabled(True)
|
||||
self.summary_label.setText(
|
||||
f"Completed: {self._done} request(s) — "
|
||||
f"Tests: {self._passed_tests}/{self._total_tests} passed"
|
||||
)
|
||||
Reference in New Issue
Block a user