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:
2026-03-28 17:34:18 +05:30
parent 1dbbb4320b
commit 01662f7e0e
37 changed files with 7822 additions and 1 deletions

222
app/ui/mock_server_panel.py Normal file
View File

@@ -0,0 +1,222 @@
"""APIClient - Agent — Mock Server Panel."""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
QTableWidget, QTableWidgetItem, QHeaderView, QDialog,
QFormLayout, QLineEdit, QComboBox, QTextEdit, QSpinBox,
QDialogButtonBox, QMessageBox
)
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QBrush, QColor
from app.ui.theme import Colors, restyle
from app.core import mock_server, storage
from app.models import MockEndpoint
class EndpointDialog(QDialog):
def __init__(self, ep: MockEndpoint = None, parent=None):
super().__init__(parent)
self.setWindowTitle("Mock Endpoint")
self.setMinimumWidth(480)
self.ep = MockEndpoint() if ep is None else MockEndpoint(
id=ep.id, name=ep.name, method=ep.method, path=ep.path,
status_code=ep.status_code, response_headers=ep.response_headers,
response_body=ep.response_body
)
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
header = QWidget()
header.setObjectName("panelHeader")
header.setFixedHeight(44)
hl = QHBoxLayout(header)
hl.setContentsMargins(16, 0, 16, 0)
title = QLabel("Configure Mock Endpoint")
title.setObjectName("panelTitle")
hl.addWidget(title)
layout.addWidget(header)
body = QWidget()
bl = QVBoxLayout(body)
form = QFormLayout()
form.setContentsMargins(16, 12, 16, 12)
form.setSpacing(10)
self.name_input = QLineEdit(self.ep.name or "")
self.name_input.setPlaceholderText("Optional display name")
self.method_combo = QComboBox()
self.method_combo.addItems(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "*"])
self.method_combo.setCurrentText(self.ep.method or "GET")
self.path_input = QLineEdit(self.ep.path or "/")
self.path_input.setPlaceholderText("/api/v1/resource")
self.status_spin = QSpinBox()
self.status_spin.setRange(100, 599)
self.status_spin.setValue(self.ep.status_code or 200)
self.body_editor = QTextEdit()
self.body_editor.setPlaceholderText('{"message": "ok"}')
self.body_editor.setPlainText(self.ep.response_body or "")
self.body_editor.setMaximumHeight(140)
form.addRow("Name:", self.name_input)
form.addRow("Method:", self.method_combo)
form.addRow("Path:", self.path_input)
form.addRow("Status Code:", self.status_spin)
form.addRow("Response Body:", self.body_editor)
bl.addLayout(form)
layout.addWidget(body, 1)
footer = QWidget()
footer.setObjectName("panelFooter")
fl = QVBoxLayout(footer)
fl.setContentsMargins(12, 8, 12, 8)
btns = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
btns.accepted.connect(self._save)
btns.rejected.connect(self.reject)
fl.addWidget(btns)
layout.addWidget(footer)
def _save(self):
path = self.path_input.text().strip()
if not path:
QMessageBox.warning(self, "Validation", "Path is required.")
return
if not path.startswith("/"):
path = "/" + path
self.ep.name = self.name_input.text().strip()
self.ep.method = self.method_combo.currentText()
self.ep.path = path
self.ep.status_code = self.status_spin.value()
self.ep.response_body = self.body_editor.toPlainText()
self.accept()
class MockServerPanel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
layout = QVBoxLayout(self)
layout.setContentsMargins(12, 12, 12, 12)
layout.setSpacing(8)
# ── Server controls ───────────────────────────────────────────────────
top = QHBoxLayout()
port_label = QLabel("Port:")
port_label.setObjectName("fieldLabel")
self.port_input = QLineEdit("8888")
self.port_input.setFixedWidth(70)
self.port_input.setToolTip("Listening port for the mock server")
self.toggle_btn = QPushButton("Start Server")
self.toggle_btn.setObjectName("accent")
self.toggle_btn.setFixedWidth(120)
self.toggle_btn.clicked.connect(self._toggle_server)
self.status_label = QLabel("● Stopped")
self.status_label.setObjectName("statusErr")
top.addWidget(port_label)
top.addWidget(self.port_input)
top.addWidget(self.toggle_btn)
top.addStretch()
top.addWidget(self.status_label)
layout.addLayout(top)
# ── Endpoint table ────────────────────────────────────────────────────
self.table = QTableWidget(0, 4)
self.table.setHorizontalHeaderLabels(["Name", "Method", "Path", "Status"])
self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents)
self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
self.table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
self.table.verticalHeader().setVisible(False)
self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
self.table.doubleClicked.connect(self._edit_endpoint)
layout.addWidget(self.table)
# ── Action buttons ────────────────────────────────────────────────────
btn_row = QHBoxLayout()
add_btn = QPushButton("+ Add Endpoint")
add_btn.clicked.connect(self._add_endpoint)
del_btn = QPushButton("Delete")
del_btn.setObjectName("danger")
del_btn.clicked.connect(self._delete_endpoint)
btn_row.addWidget(add_btn)
btn_row.addWidget(del_btn)
btn_row.addStretch()
layout.addLayout(btn_row)
self._load_endpoints()
# ── Data loading ──────────────────────────────────────────────────────────
def _load_endpoints(self):
self.table.setRowCount(0)
for ep in storage.get_mock_endpoints():
row = self.table.rowCount()
self.table.insertRow(row)
self.table.setItem(row, 0, QTableWidgetItem(ep.name or ""))
method_item = QTableWidgetItem(ep.method)
method_item.setForeground(QBrush(QColor(Colors.INFO)))
self.table.setItem(row, 1, method_item)
self.table.setItem(row, 2, QTableWidgetItem(ep.path))
self.table.setItem(row, 3, QTableWidgetItem(str(ep.status_code)))
self.table.item(row, 0).setData(Qt.ItemDataRole.UserRole, ep)
# ── Actions ───────────────────────────────────────────────────────────────
def _add_endpoint(self):
dlg = EndpointDialog(parent=self)
if dlg.exec():
storage.save_mock_endpoint(dlg.ep)
self._load_endpoints()
def _edit_endpoint(self):
row = self.table.currentRow()
if row < 0:
return
item = self.table.item(row, 0)
if not item:
return
ep = item.data(Qt.ItemDataRole.UserRole)
dlg = EndpointDialog(ep=ep, parent=self)
if dlg.exec():
storage.save_mock_endpoint(dlg.ep)
self._load_endpoints()
def _delete_endpoint(self):
row = self.table.currentRow()
if row < 0:
return
item = self.table.item(row, 0)
if not item:
return
ep = item.data(Qt.ItemDataRole.UserRole)
if ep and ep.id:
storage.delete_mock_endpoint(ep.id)
self._load_endpoints()
def _toggle_server(self):
if mock_server.is_running():
mock_server.stop()
self.toggle_btn.setText("Start Server")
restyle(self.status_label, "statusErr")
self.status_label.setText("● Stopped")
else:
try:
port = int(self.port_input.text())
except ValueError:
port = 8888
msg = mock_server.start(port)
if mock_server.is_running():
self.toggle_btn.setText("Stop Server")
restyle(self.status_label, "statusOk")
self.status_label.setText(f"● Running on :{port}")
else:
QMessageBox.warning(self, "Mock Server", msg)