Update documentation.

This commit is contained in:
2026-03-28 17:42:37 +05:30
parent 01662f7e0e
commit 79b120ff91
25 changed files with 109 additions and 109 deletions

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Conversational AI co-pilot core."""
"""APIClient - Agent - Conversational AI co-pilot core."""
import json
import re
import httpx
@@ -11,9 +11,9 @@ You are APIClient - Agent, an expert AI API testing co-pilot embedded in the API
Your responsibilities:
• Help craft and debug HTTP requests (REST, JSON-API, GraphQL, Odoo APIs)
• Analyze HTTP responses status codes, headers, body structure, errors
• Analyze HTTP responses - status codes, headers, body structure, errors
• Specialize in the EKIKA Odoo API Framework:
- JSON-API (Content-Type: application/vnd.api+json) body format: {"data": {"type": model, "attributes": {...}}}
- JSON-API (Content-Type: application/vnd.api+json) - body format: {"data": {"type": model, "attributes": {...}}}
- REST JSON (Content-Type: application/json)
- GraphQL (POST with {"query": "..."} body)
- Auth: x-api-key header, Basic Auth, OAuth2 Bearer, JWT Bearer
@@ -45,7 +45,7 @@ pm.test('Has data', lambda: expect(pm.response.json()).to_have_key('data'))
```
Rules:
- Be concise and actionable explain WHY, not just WHAT
- Be concise and actionable - explain WHY, not just WHAT
- If you add apply blocks, briefly explain what each block does
- For JSON-API responses: data is in response.data, errors in response.errors
- For SSL cert errors: tell user to uncheck SSL verification in the Settings tab

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Claude AI integration."""
"""APIClient - Agent - Claude AI integration."""
import json
import re
import httpx
@@ -28,7 +28,7 @@ You are an expert API documentation analyzer for APIClient - Agent.
Given API documentation (which may be a spec, a web page, framework docs, or raw text),
extract or infer all useful API endpoints and return structured JSON.
Return ONLY valid JSON no markdown, no commentary, just the JSON object.
Return ONLY valid JSON - no markdown, no commentary, just the JSON object.
Schema:
{
@@ -74,7 +74,7 @@ Rules:
- If it is a GRAPHQL API, generate a POST /graphql endpoint with example query body
- If auth options are shown (API key, OAuth, Basic), include ALL variants as separate
environment variables so the user can choose
- Keep paths clean strip trailing slashes, normalise to lowercase
- Keep paths clean - strip trailing slashes, normalise to lowercase
"""
@@ -202,7 +202,7 @@ def fetch_url_content(url: str) -> str:
ct = resp.headers.get("content-type", "")
text = resp.text
# If HTML page strip tags for cleaner AI input
# If HTML page - strip tags for cleaner AI input
if "html" in ct and not _looks_like_spec(text):
text = _strip_html(text)

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Code snippet generators."""
"""APIClient - Agent - Code snippet generators."""
import json
from urllib.parse import urlencode

View File

@@ -1,4 +1,4 @@
"""EKIKA Odoo API Framework Direct collection generator.
"""EKIKA Odoo API Framework - Direct collection generator.
Generates complete Postman-style collections from the EKIKA api_framework module
without requiring any AI API calls. All URL patterns, body formats, auth headers,
@@ -87,7 +87,7 @@ def _env_vars(instance_url: str, auth_type: str, extra: dict = None) -> dict:
def _clean_endpoint(endpoint: str) -> str:
"""Normalise endpoint slug ensure leading slash, strip trailing slash."""
"""Normalise endpoint slug - ensure leading slash, strip trailing slash."""
ep = endpoint.strip().strip("/")
return f"/{ep}" if ep else "/api"
@@ -281,7 +281,7 @@ def _build_jsonapi_endpoints(base_ep: str, model: str, headers: dict,
if "Get Fields" in operations:
eps.append({
"name": f"Get Fields {model}",
"name": f"Get Fields - {model}",
"method": "GET",
"path": f"{ep_path}/fields_get",
"headers": {**headers, "Accept": ct},
@@ -295,7 +295,7 @@ def _build_jsonapi_endpoints(base_ep: str, model: str, headers: dict,
if "Check Access Rights" in operations:
eps.append({
"name": f"Check Access {model}",
"name": f"Check Access - {model}",
"method": "GET",
"path": f"{ep_path}/check_access_rights",
"headers": {**headers, "Accept": ct},
@@ -405,7 +405,7 @@ def _build_restjson_endpoints(base_ep: str, model: str, headers: dict,
if "Get Fields" in operations:
eps.append({
"name": f"Get Fields {model}",
"name": f"Get Fields - {model}",
"method": "GET",
"path": f"{ep_path}/fields_get",
"headers": {**headers},
@@ -442,7 +442,7 @@ def _build_graphql_endpoints(base_ep: str, model: str, headers: dict,
f"}}"
)
eps.append({
"name": f"GraphQL List {model}",
"name": f"GraphQL - List {model}",
"method": "POST",
"path": path,
"headers": {**headers, "Content-Type": ct},
@@ -465,7 +465,7 @@ def _build_graphql_endpoints(base_ep: str, model: str, headers: dict,
f"}}"
)
eps.append({
"name": f"GraphQL Get {model} by ID",
"name": f"GraphQL - Get {model} by ID",
"method": "POST",
"path": path,
"headers": {**headers, "Content-Type": ct},
@@ -491,7 +491,7 @@ def _build_graphql_endpoints(base_ep: str, model: str, headers: dict,
f"}}"
)
eps.append({
"name": f"GraphQL Create {model}",
"name": f"GraphQL - Create {model}",
"method": "POST",
"path": path,
"headers": {**headers, "Content-Type": ct},
@@ -518,7 +518,7 @@ def _build_graphql_endpoints(base_ep: str, model: str, headers: dict,
f"}}"
)
eps.append({
"name": f"GraphQL Update {model}",
"name": f"GraphQL - Update {model}",
"method": "POST",
"path": path,
"headers": {**headers, "Content-Type": ct},
@@ -539,7 +539,7 @@ def _build_graphql_endpoints(base_ep: str, model: str, headers: dict,
f"}}"
)
eps.append({
"name": f"GraphQL Delete {model}",
"name": f"GraphQL - Delete {model}",
"method": "POST",
"path": path,
"headers": {**headers, "Content-Type": ct},
@@ -587,7 +587,7 @@ def generate_collection(
all_endpoints += _build_restjson_endpoints(base_ep, model, headers, operations)
elif api_kind == "GraphQL":
all_endpoints += _build_graphql_endpoints(base_ep, model, headers, operations)
else: # Custom REST JSON same as REST JSON
else: # Custom REST JSON - same as REST JSON
all_endpoints += _build_restjson_endpoints(base_ep, model, headers, operations)
# Build URLs using {{base_url}} variable
@@ -595,7 +595,7 @@ def generate_collection(
if not ep["path"].startswith("http"):
ep["url"] = f"{{{{base_url}}}}{ep['path']}"
name = collection_name or f"EKIKA Odoo {api_kind} {', '.join(models[:3])}"
name = collection_name or f"EKIKA Odoo - {api_kind} - {', '.join(models[:3])}"
return {
"collection_name": name,

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent HTTP client engine."""
"""APIClient - Agent - HTTP client engine."""
import re
import base64
from copy import deepcopy
@@ -144,18 +144,18 @@ def send_request(req: HttpRequest, variables: dict = None) -> HttpResponse:
detail = str(e)
if "CERTIFICATE_VERIFY_FAILED" in detail or "certificate" in detail.lower() or "SSL" in detail:
return HttpResponse(error=(
f"SSL certificate error could not connect to {r.url}\n\n"
f"SSL certificate error - could not connect to {r.url}\n\n"
f"The server's certificate is not trusted or doesn't match the hostname.\n"
f"Tip: disable SSL verification in the request Settings tab."
))
return HttpResponse(error=f"Connection refused could not reach {r.url}")
return HttpResponse(error=f"Connection refused - could not reach {r.url}")
except httpx.ConnectTimeout:
return HttpResponse(error=f"Connection timed out after {req.timeout}s")
except httpx.ReadTimeout:
return HttpResponse(error=f"Read timed out server took too long to respond")
return HttpResponse(error=f"Read timed out - server took too long to respond")
except httpx.SSLError as e:
return HttpResponse(error=f"SSL error: {e}. Disable SSL verification if using a self-signed cert.")
except httpx.TooManyRedirects:
return HttpResponse(error="Too many redirects possible redirect loop")
return HttpResponse(error="Too many redirects - possible redirect loop")
except Exception as e:
return HttpResponse(error=str(e))

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Lightweight HTTP mock server."""
"""APIClient - Agent - Lightweight HTTP mock server."""
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent OpenAPI / Swagger spec parser.
"""APIClient - Agent - OpenAPI / Swagger spec parser.
Parses OpenAPI 3.x and Swagger 2.0 specs (JSON or YAML) directly,
without needing AI tokens.

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Storage layer (SQLite)."""
"""APIClient - Agent - Storage layer (SQLite)."""
import json
import sqlite3
from pathlib import Path

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Core data models."""
"""APIClient - Agent - Core data models."""
from dataclasses import dataclass, field
from typing import Optional

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent AI chat sidebar panel (persistent, context-aware)."""
"""APIClient - Agent - AI chat sidebar panel (persistent, context-aware)."""
import re
from PyQt6.QtWidgets import (
@@ -124,7 +124,7 @@ class MessageBubble(QFrame):
self._text_lbl.setText(self._full_text)
def finalize(self):
"""Called when streaming ends strip apply blocks and render them."""
"""Called when streaming ends - strip apply blocks and render them."""
if self._finalized:
return
self._finalized = True

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent AI Assistant Dialog."""
"""APIClient - Agent - AI Assistant Dialog."""
import json
from PyQt6.QtWidgets import (
@@ -43,7 +43,7 @@ class AnalysisWorker(QThread):
self.progress.emit("Checking for OpenAPI/Swagger spec…")
spec = openapi_parser.detect_spec(content)
if spec:
self.progress.emit("OpenAPI spec detected parsing directly…")
self.progress.emit("OpenAPI spec detected - parsing directly…")
result = openapi_parser.parse_spec(spec)
if self.base_url:
result["base_url"] = self.base_url
@@ -136,7 +136,7 @@ class AIAssistantDialog(QDialog):
layout.addWidget(footer)
# ══════════════════════════════════════════════════════════════════════════
# Tab 1 EKIKA Odoo API Framework (dedicated, no AI tokens needed)
# Tab 1 - EKIKA Odoo API Framework (dedicated, no AI tokens needed)
# ══════════════════════════════════════════════════════════════════════════
def _build_ekika_tab(self) -> QWidget:
@@ -303,7 +303,7 @@ class AIAssistantDialog(QDialog):
self.ek_preview.setFont(QFont("JetBrains Mono, Fira Code, Consolas", 10))
self.ek_preview.setPlaceholderText(
"Fill in the form above and click Generate Collection to preview.\n\n"
"No API key required collection is generated instantly from the\n"
"No API key required - collection is generated instantly from the\n"
"EKIKA Odoo API Framework documentation."
)
self.ek_preview.setMaximumHeight(180)
@@ -380,7 +380,7 @@ class AIAssistantDialog(QDialog):
self.ek_import_btn.setEnabled(True)
self.ek_env_btn.setEnabled(True)
self.ek_both_btn.setEnabled(True)
self.status_label.setText(f"{len(eps)} endpoint(s) ready click Import to save")
self.status_label.setText(f"{len(eps)} endpoint(s) ready - click Import to save")
def _ekika_import(self):
if not self._result:
@@ -399,7 +399,7 @@ class AIAssistantDialog(QDialog):
self._do_create_env(self._result)
# ══════════════════════════════════════════════════════════════════════════
# Tab 2 Generic AI analysis (OpenAPI / any docs URL)
# Tab 2 - Generic AI analysis (OpenAPI / any docs URL)
# ══════════════════════════════════════════════════════════════════════════
def _build_generic_tab(self) -> QWidget:
@@ -543,10 +543,10 @@ class AIAssistantDialog(QDialog):
lines = [
f"✓ Parsed via: {src_label}",
f"✓ Collection: {result.get('collection_name', 'Unnamed')}",
f"✓ Base URL: {result.get('base_url', '')}",
f"✓ Base URL: {result.get('base_url', '-')}",
f"✓ Auth type: {result.get('auth_type', 'none')}",
f"✓ Endpoints: {len(endpoints)} found",
f"✓ Env vars: {list(env_vars.keys()) or ''}",
f"✓ Env vars: {list(env_vars.keys()) or '-'}",
]
if notes:
lines += ["", "── Notes ─────────────────", notes]
@@ -565,7 +565,7 @@ class AIAssistantDialog(QDialog):
self.analyze_btn.setEnabled(True)
self.progress_bar.setVisible(False)
self.result_view.setPlainText(f"✗ Error:\n\n{msg}")
self.status_label.setText("Error see results panel")
self.status_label.setText("Error - see results panel")
def _set_generic_action_buttons(self, enabled: bool):
self.import_btn.setEnabled(enabled)
@@ -586,7 +586,7 @@ class AIAssistantDialog(QDialog):
self._do_create_env(self._generic_result)
# ══════════════════════════════════════════════════════════════════════════
# Tab 3 Settings
# Tab 3 - Settings
# ══════════════════════════════════════════════════════════════════════════
def _build_settings_tab(self) -> QWidget:
@@ -598,7 +598,7 @@ class AIAssistantDialog(QDialog):
hint = QLabel(
"EKIKA AI Assistant uses Claude by Anthropic to analyze plain-text API documentation.\n"
"OpenAPI/Swagger specs and EKIKA Odoo Framework collections are generated locally "
"OpenAPI/Swagger specs and EKIKA Odoo Framework collections are generated locally - "
"no API key required for those."
)
hint.setObjectName("hintText")
@@ -683,7 +683,7 @@ class AIAssistantDialog(QDialog):
def _do_create_env(self, result: dict):
env_vars = result.get("environment_variables", {})
col_name = result.get("collection_name", "AI Import")
env_name = f"{col_name} Environment"
env_name = f"{col_name} - Environment"
if not env_vars:
QMessageBox.information(self, "No Variables", "No environment variables detected.")

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Code Generation Dialog."""
"""APIClient - Agent - Code Generation Dialog."""
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QComboBox,
QTextEdit, QPushButton, QLabel, QApplication, QWidget

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Collection Runner dialog."""
"""APIClient - Agent - Collection Runner dialog."""
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QTreeWidget, QTreeWidgetItem, QComboBox, QHeaderView, QProgressBar, QWidget
@@ -168,7 +168,7 @@ class CollectionRunnerDialog(QDialog):
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 ""
test_str = f"{passed}/{total}" if total > 0 else "-"
item = QTreeWidgetItem([
f"{result.method} {result.request_name}",
status_str,
@@ -189,6 +189,6 @@ class CollectionRunnerDialog(QDialog):
def _on_finished(self):
self.run_btn.setEnabled(True)
self.summary_label.setText(
f"Completed: {self._done} request(s) "
f"Completed: {self._done} request(s) - "
f"Tests: {self._passed_tests}/{self._total_tests} passed"
)

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Environment Manager Dialog."""
"""APIClient - Agent - Environment Manager Dialog."""
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem,
QPushButton, QTableWidget, QTableWidgetItem, QHeaderView,

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Import Dialog."""
"""APIClient - Agent - Import Dialog."""
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget,
QTextEdit, QPushButton, QLabel, QFileDialog, QMessageBox
@@ -76,7 +76,7 @@ class ImportDialog(QDialog):
layout.setContentsMargins(16, 12, 16, 12)
layout.setSpacing(8)
hint = QLabel("Paste a cURL command it will open as a new request tab:")
hint = QLabel("Paste a cURL command - it will open as a new request tab:")
hint.setObjectName("hintText")
layout.addWidget(hint)

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Main Window."""
"""APIClient - Agent - Main Window."""
from PyQt6.QtWidgets import (
QMainWindow, QSplitter, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QComboBox, QPushButton, QStatusBar, QTabWidget,
@@ -147,7 +147,7 @@ class MainWindow(QMainWindow):
self.chat_panel = AIChatPanel()
splitter.addWidget(self.chat_panel)
splitter.setSizes([260, 940, 360]) # give chat panel real size first
self.chat_panel.hide() # THEN hide splitter remembers 360
self.chat_panel.hide() # THEN hide - splitter remembers 360
self._main_splitter = splitter
# Wire apply signals
@@ -163,7 +163,7 @@ class MainWindow(QMainWindow):
self._status_bar = QStatusBar()
self._status_bar.setFixedHeight(26)
self.setStatusBar(self._status_bar)
self._status_bar.showMessage(f"Ready {APP_NAME} v{APP_VERSION}")
self._status_bar.showMessage(f"Ready - {APP_NAME} v{APP_VERSION}")
def _build_http_workspace(self) -> QWidget:
w = QWidget()

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Mock Server Panel."""
"""APIClient - Agent - Mock Server Panel."""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
QTableWidget, QTableWidgetItem, QHeaderView, QDialog,

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Request Panel."""
"""APIClient - Agent - Request Panel."""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QComboBox, QLineEdit,
QPushButton, QTabWidget, QTableWidget, QTableWidgetItem,
@@ -227,7 +227,7 @@ class RequestPanel(QWidget):
self.url_input = QLineEdit()
self.url_input.setObjectName("urlBar")
self.url_input.setPlaceholderText("Enter URL e.g. https://api.example.com/v1/users")
self.url_input.setPlaceholderText("Enter URL - e.g. https://api.example.com/v1/users")
self.url_input.returnPressed.connect(self._send)
self.send_btn = QPushButton("Send")
@@ -359,7 +359,7 @@ class RequestPanel(QWidget):
# ── Slots ────────────────────────────────────────────────────────────────
def _on_method_changed(self, method: str):
# Inline style is intentional here color is dynamic per method value
# Inline style is intentional here - color is dynamic per method value
color = method_color(method)
self.method_combo.setStyleSheet(f"QComboBox#methodCombo {{ color: {color}; }}")
@@ -377,7 +377,7 @@ class RequestPanel(QWidget):
parsed = json.loads(text)
self.body_editor.setPlainText(json.dumps(parsed, indent=2, ensure_ascii=False))
except json.JSONDecodeError:
pass # not valid JSON leave as-is
pass # not valid JSON - leave as-is
def _send(self):
self.send_requested.emit(self._build_request())

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Response Panel."""
"""APIClient - Agent - Response Panel."""
import json
from PyQt6.QtWidgets import (
@@ -25,13 +25,13 @@ def _fmt_size(n: int) -> str:
class StatusBadge(QLabel):
def __init__(self, parent=None):
super().__init__("", parent)
super().__init__("-", parent)
self.setFixedHeight(26)
self._apply_style(Colors.TEXT_MUTED)
self.setFont(QFont("Segoe UI", 11, QFont.Weight.Bold))
def _apply_style(self, color: str):
# Inline style intentional badge color is dynamic per status code
# Inline style intentional - badge color is dynamic per status code
self.setStyleSheet(f"""
QLabel {{
color: {color};
@@ -53,7 +53,7 @@ class StatusBadge(QLabel):
self._apply_style(Colors.ERROR)
def clear(self):
self.setText("")
self.setText("-")
self._apply_style(Colors.TEXT_MUTED)
@@ -174,8 +174,8 @@ class ResponsePanel(QWidget):
self._loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
ll.addWidget(self._loading_label)
self._stack.addWidget(self.tabs) # index 0 normal view
self._stack.addWidget(loading_widget) # index 1 loading
self._stack.addWidget(self.tabs) # index 0 - normal view
self._stack.addWidget(loading_widget) # index 1 - loading
layout.addWidget(self._stack, 1)
@@ -203,7 +203,7 @@ class ResponsePanel(QWidget):
size = resp.size_bytes or len(resp.body.encode())
self.size_label.setText(_fmt_size(size))
# Body pretty-print JSON if possible
# Body - pretty-print JSON if possible
try:
parsed = json.loads(resp.body)
self.body_view.setPlainText(json.dumps(parsed, indent=2, ensure_ascii=False))

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Request Search Dialog."""
"""APIClient - Agent - Request Search Dialog."""
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLineEdit,
QPushButton, QListWidget, QListWidgetItem, QLabel, QWidget

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Collections Sidebar."""
"""APIClient - Agent - Collections Sidebar."""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem,
QPushButton, QInputDialog, QMenu, QLineEdit, QLabel, QMessageBox

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Multi-tab request manager."""
"""APIClient - Agent - Multi-tab request manager."""
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QTabWidget, QPushButton, QTabBar
from PyQt6.QtCore import pyqtSignal, Qt

View File

@@ -1,7 +1,7 @@
"""
APIClient - Agent Central Theme Engine
APIClient - Agent - Central Theme Engine
All styling lives here in the global QSS.
UI widgets use setObjectName() selectors never inline setStyleSheet() for static colors.
UI widgets use setObjectName() selectors - never inline setStyleSheet() for static colors.
Only truly dynamic values (per-request method color, status badge) stay inline.
"""
from PyQt6.QtGui import QColor, QPalette

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent WebSocket client panel."""
"""APIClient - Agent - WebSocket client panel."""
import asyncio
import queue
import time