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,6 +1,6 @@
# APIClient - Agent # APIClient - Agent
> **AI-first API testing desktop client** built with Python + PyQt6. > **AI-first API testing desktop client** - built with Python + PyQt6.
> Specialised for the [EKIKA Odoo API Framework](https://apps.odoo.com/apps/modules/19.0/api_framework), but works with any REST, GraphQL, or WebSocket API. > Specialised for the [EKIKA Odoo API Framework](https://apps.odoo.com/apps/modules/19.0/api_framework), but works with any REST, GraphQL, or WebSocket API.
--- ---
@@ -8,31 +8,31 @@
## Features ## Features
### Core API Testing ### Core API Testing
- **Multi-tab request editor** work on multiple requests simultaneously, drag to reorder - **Multi-tab request editor** - work on multiple requests simultaneously, drag to reorder
- **All HTTP methods** GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS - **All HTTP methods** - GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
- **Smart params & headers table** per-row enable/disable checkboxes, 36 px comfortable rows, auto-expanding blank row - **Smart params & headers table** - per-row enable/disable checkboxes, 36 px comfortable rows, auto-expanding blank row
- **Body editor** raw JSON/XML/text with syntax highlighting, **Format JSON** button, `application/vnd.api+json` support - **Body editor** - raw JSON/XML/text with syntax highlighting, **Format JSON** button, `application/vnd.api+json` support
- **Auth panel** Bearer Token, Basic Auth, API Key (header or query) - **Auth panel** - Bearer Token, Basic Auth, API Key (header or query)
- **Pre-request scripts** Python executed before each request; access `pm.environment.get/set` - **Pre-request scripts** - Python executed before each request; access `pm.environment.get/set`
- **Test scripts** assertions auto-run after every response; `pm.test(...)` / `expect(...)` DSL - **Test scripts** - assertions auto-run after every response; `pm.test(...)` / `expect(...)` DSL
- **Response viewer** syntax-highlighted body, headers table, test results, search, copy, save - **Response viewer** - syntax-highlighted body, headers table, test results, search, copy, save
- **WebSocket client** connect, send, receive, log messages - **WebSocket client** - connect, send, receive, log messages
- **Mock server** local HTTP mock with configurable routes - **Mock server** - local HTTP mock with configurable routes
### Collections & Environments ### Collections & Environments
- **Collections sidebar** import/export Postman Collection v2.1 JSON, cURL - **Collections sidebar** - import/export Postman Collection v2.1 JSON, cURL
- **Environment variables** `{{base_url}}`, `{{api_key}}`, etc. resolved at send time; per-environment values - **Environment variables** - `{{base_url}}`, `{{api_key}}`, etc. resolved at send time; per-environment values
- **Collection runner** run all requests in a collection, view pass/fail results - **Collection runner** - run all requests in a collection, view pass/fail results
- **History** every sent request automatically saved - **History** - every sent request automatically saved
### AI Co-pilot (Claude-powered) ### AI Co-pilot (Claude-powered)
- **Persistent AI chat sidebar** toggle with the `✦ AI` button or `Ctrl+Shift+A` - **Persistent AI chat sidebar** - toggle with the `✦ AI` button or `Ctrl+Shift+A`
- **Full context awareness** AI sees your current request (method, URL, headers, body, params, test scripts) and the last response (status, body, errors); secrets are automatically redacted - **Full context awareness** - AI sees your current request (method, URL, headers, body, params, test scripts) and the last response (status, body, errors); secrets are automatically redacted
- **Streaming responses** tokens stream in real time - **Streaming responses** - tokens stream in real time
- **One-click Apply** AI suggestions come with **Apply Body**, **Apply Params**, **Apply Headers**, **Apply Test Script** buttons that set the values directly in the request editor - **One-click Apply** - AI suggestions come with **Apply Body**, **Apply Params**, **Apply Headers**, **Apply Test Script** buttons that set the values directly in the request editor
- **Multi-turn conversation** full history maintained per session; Clear to reset - **Multi-turn conversation** - full history maintained per session; Clear to reset
- **Quick actions** Analyze, Fix Error, Gen Body, Write Tests, Auth Help, Explain Response - **Quick actions** - Analyze, Fix Error, Gen Body, Write Tests, Auth Help, Explain Response
- **EKIKA Odoo collection generator** generate complete collections for JSON-API, REST JSON, GraphQL, and Custom REST JSON without spending AI tokens; supports all auth types - **EKIKA Odoo collection generator** - generate complete collections for JSON-API, REST JSON, GraphQL, and Custom REST JSON without spending AI tokens; supports all auth types
### EKIKA Odoo API Framework specialisation ### EKIKA Odoo API Framework specialisation
- Generates full CRUD + Execute / Export / Report / Fields / Access-Rights endpoints per model - Generates full CRUD + Execute / Export / Report / Fields / Access-Rights endpoints per model
@@ -73,14 +73,14 @@ pyinstaller>=6.0.0
## Quick Start ## Quick Start
### 1 Send your first request ### 1 - Send your first request
1. Launch the app: `python main.py` 1. Launch the app: `python main.py`
2. Type a URL in the bar, e.g. `https://jsonplaceholder.typicode.com/todos/1` 2. Type a URL in the bar, e.g. `https://jsonplaceholder.typicode.com/todos/1`
3. Press **Send** (or `Ctrl+Enter`) 3. Press **Send** (or `Ctrl+Enter`)
4. See the JSON response with syntax highlighting in the bottom panel 4. See the JSON response with syntax highlighting in the bottom panel
### 2 Use environment variables ### 2 - Use environment variables
1. Click **Manage****New Environment** → name it `My API` 1. Click **Manage****New Environment** → name it `My API`
2. Add variables: 2. Add variables:
@@ -93,7 +93,7 @@ pyinstaller>=6.0.0
5. In Headers, add `Authorization: Bearer {{api_key}}` 5. In Headers, add `Authorization: Bearer {{api_key}}`
6. Variables are resolved automatically at send time 6. Variables are resolved automatically at send time
### 3 Import a collection ### 3 - Import a collection
**From Postman export:** **From Postman export:**
1. `File → Import…` 1. `File → Import…`
@@ -109,11 +109,11 @@ curl -X POST https://api.example.com/v1/orders \
**From OpenAPI spec:** **From OpenAPI spec:**
1. `Tools → AI Assistant → Import from Docs` 1. `Tools → AI Assistant → Import from Docs`
2. Paste the OpenAPI JSON/YAML URL parsed instantly, no AI tokens used 2. Paste the OpenAPI JSON/YAML URL - parsed instantly, no AI tokens used
--- ---
## EKIKA Odoo API Framework Complete Example ## EKIKA Odoo API Framework - Complete Example
### Generate a collection in 30 seconds ### Generate a collection in 30 seconds
@@ -131,8 +131,8 @@ curl -X POST https://api.example.com/v1/orders \
| Models | `sale.order, res.partner, account.move` | | Models | `sale.order, res.partner, account.move` |
| Operations | ✓ List, Get, Create, Update, Delete | | Operations | ✓ List, Get, Create, Update, Delete |
4. Click **Generate Collection** preview appears instantly 4. Click **Generate Collection** - preview appears instantly
5. Click **Import Both** collection + environment are saved 5. Click **Import Both** - collection + environment are saved
This generates the following requests for each model with zero AI tokens: This generates the following requests for each model with zero AI tokens:
@@ -200,7 +200,7 @@ Select **API Kind: GraphQL**. The generator creates:
--- ---
## AI Chat Co-pilot Example Session ## AI Chat Co-pilot - Example Session
Click **✦ AI** in the top bar to open the sidebar. The AI automatically knows what request you have open and the last response. Click **✦ AI** in the top bar to open the sidebar. The AI automatically knows what request you have open and the last response.
@@ -227,7 +227,7 @@ AI: A 401 on the EKIKA JSON-API endpoint means the x-api-key header is
[ Apply Headers to Request ] [ Apply Headers to Request ]
``` ```
Click **Apply Headers to Request** headers are set immediately and the Headers tab opens. Click **Apply Headers to Request** - headers are set immediately and the Headers tab opens.
### Generating a body for a complex model ### Generating a body for a complex model
@@ -305,8 +305,8 @@ APIClient-Agent/
│ ├── core/ │ ├── core/
│ │ ├── storage.py # SQLite persistence (collections, environments, history) │ │ ├── storage.py # SQLite persistence (collections, environments, history)
│ │ ├── http_client.py # httpx-based request engine, variable resolution │ │ ├── http_client.py # httpx-based request engine, variable resolution
│ │ ├── ai_client.py # Claude API collection generation from docs │ │ ├── ai_client.py # Claude API - collection generation from docs
│ │ ├── ai_chat.py # Claude API multi-turn conversational co-pilot │ │ ├── ai_chat.py # Claude API - multi-turn conversational co-pilot
│ │ ├── openapi_parser.py # OpenAPI 3.x / Swagger 2.0 local parser │ │ ├── openapi_parser.py # OpenAPI 3.x / Swagger 2.0 local parser
│ │ ├── ekika_odoo_generator.py# EKIKA Odoo framework collection generator │ │ ├── ekika_odoo_generator.py# EKIKA Odoo framework collection generator
│ │ ├── test_runner.py # pm.test / expect assertion engine │ │ ├── test_runner.py # pm.test / expect assertion engine
@@ -345,7 +345,7 @@ Settings are stored in an SQLite database at `~/.apiclient_agent/data.db` (creat
2. In the app: `Tools → AI Assistant → Settings tab` 2. In the app: `Tools → AI Assistant → Settings tab`
3. Paste the key and click **Save API Key** 3. Paste the key and click **Save API Key**
The key is stored locally in the SQLite database only never transmitted except to the Anthropic API. The key is stored locally in the SQLite database only - never transmitted except to the Anthropic API.
--- ---
@@ -354,7 +354,7 @@ The key is stored locally in the SQLite database only — never transmitted exce
Some servers (especially demo/development instances) use self-signed certificates or wildcard certificates that don't match the exact hostname. If you see: Some servers (especially demo/development instances) use self-signed certificates or wildcard certificates that don't match the exact hostname. If you see:
``` ```
SSL certificate error could not connect to https://... SSL certificate error - could not connect to https://...
Tip: disable SSL verification in the request Settings tab. Tip: disable SSL verification in the request Settings tab.
``` ```
@@ -382,10 +382,10 @@ The executable is produced in `dist/APIClient-Agent`.
3. Commit your changes: `git commit -m "Add my feature"` 3. Commit your changes: `git commit -m "Add my feature"`
4. Push and open a pull request 4. Push and open a pull request
Please keep UI styling in `theme.py` using `setObjectName()` selectors never inline `setStyleSheet()` for static colors. Please keep UI styling in `theme.py` using `setObjectName()` selectors - never inline `setStyleSheet()` for static colors.
--- ---
## License ## License
[MIT License](LICENSE) Copyright (c) 2026 EKIKA.co [MIT License](LICENSE) - Copyright (c) 2026 EKIKA.co

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Conversational AI co-pilot core.""" """APIClient - Agent - Conversational AI co-pilot core."""
import json import json
import re import re
import httpx import httpx
@@ -11,9 +11,9 @@ You are APIClient - Agent, an expert AI API testing co-pilot embedded in the API
Your responsibilities: Your responsibilities:
• Help craft and debug HTTP requests (REST, JSON-API, GraphQL, Odoo APIs) • 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: • 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) - REST JSON (Content-Type: application/json)
- GraphQL (POST with {"query": "..."} body) - GraphQL (POST with {"query": "..."} body)
- Auth: x-api-key header, Basic Auth, OAuth2 Bearer, JWT Bearer - 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: 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 - 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 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 - 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 json
import re import re
import httpx 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), 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. 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: Schema:
{ {
@@ -74,7 +74,7 @@ Rules:
- If it is a GRAPHQL API, generate a POST /graphql endpoint with example query body - 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 - If auth options are shown (API key, OAuth, Basic), include ALL variants as separate
environment variables so the user can choose 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", "") ct = resp.headers.get("content-type", "")
text = resp.text 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): if "html" in ct and not _looks_like_spec(text):
text = _strip_html(text) text = _strip_html(text)

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Code snippet generators.""" """APIClient - Agent - Code snippet generators."""
import json import json
from urllib.parse import urlencode 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 Generates complete Postman-style collections from the EKIKA api_framework module
without requiring any AI API calls. All URL patterns, body formats, auth headers, 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: 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("/") ep = endpoint.strip().strip("/")
return f"/{ep}" if ep else "/api" 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: if "Get Fields" in operations:
eps.append({ eps.append({
"name": f"Get Fields {model}", "name": f"Get Fields - {model}",
"method": "GET", "method": "GET",
"path": f"{ep_path}/fields_get", "path": f"{ep_path}/fields_get",
"headers": {**headers, "Accept": ct}, "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: if "Check Access Rights" in operations:
eps.append({ eps.append({
"name": f"Check Access {model}", "name": f"Check Access - {model}",
"method": "GET", "method": "GET",
"path": f"{ep_path}/check_access_rights", "path": f"{ep_path}/check_access_rights",
"headers": {**headers, "Accept": ct}, "headers": {**headers, "Accept": ct},
@@ -405,7 +405,7 @@ def _build_restjson_endpoints(base_ep: str, model: str, headers: dict,
if "Get Fields" in operations: if "Get Fields" in operations:
eps.append({ eps.append({
"name": f"Get Fields {model}", "name": f"Get Fields - {model}",
"method": "GET", "method": "GET",
"path": f"{ep_path}/fields_get", "path": f"{ep_path}/fields_get",
"headers": {**headers}, "headers": {**headers},
@@ -442,7 +442,7 @@ def _build_graphql_endpoints(base_ep: str, model: str, headers: dict,
f"}}" f"}}"
) )
eps.append({ eps.append({
"name": f"GraphQL List {model}", "name": f"GraphQL - List {model}",
"method": "POST", "method": "POST",
"path": path, "path": path,
"headers": {**headers, "Content-Type": ct}, "headers": {**headers, "Content-Type": ct},
@@ -465,7 +465,7 @@ def _build_graphql_endpoints(base_ep: str, model: str, headers: dict,
f"}}" f"}}"
) )
eps.append({ eps.append({
"name": f"GraphQL Get {model} by ID", "name": f"GraphQL - Get {model} by ID",
"method": "POST", "method": "POST",
"path": path, "path": path,
"headers": {**headers, "Content-Type": ct}, "headers": {**headers, "Content-Type": ct},
@@ -491,7 +491,7 @@ def _build_graphql_endpoints(base_ep: str, model: str, headers: dict,
f"}}" f"}}"
) )
eps.append({ eps.append({
"name": f"GraphQL Create {model}", "name": f"GraphQL - Create {model}",
"method": "POST", "method": "POST",
"path": path, "path": path,
"headers": {**headers, "Content-Type": ct}, "headers": {**headers, "Content-Type": ct},
@@ -518,7 +518,7 @@ def _build_graphql_endpoints(base_ep: str, model: str, headers: dict,
f"}}" f"}}"
) )
eps.append({ eps.append({
"name": f"GraphQL Update {model}", "name": f"GraphQL - Update {model}",
"method": "POST", "method": "POST",
"path": path, "path": path,
"headers": {**headers, "Content-Type": ct}, "headers": {**headers, "Content-Type": ct},
@@ -539,7 +539,7 @@ def _build_graphql_endpoints(base_ep: str, model: str, headers: dict,
f"}}" f"}}"
) )
eps.append({ eps.append({
"name": f"GraphQL Delete {model}", "name": f"GraphQL - Delete {model}",
"method": "POST", "method": "POST",
"path": path, "path": path,
"headers": {**headers, "Content-Type": ct}, "headers": {**headers, "Content-Type": ct},
@@ -587,7 +587,7 @@ def generate_collection(
all_endpoints += _build_restjson_endpoints(base_ep, model, headers, operations) all_endpoints += _build_restjson_endpoints(base_ep, model, headers, operations)
elif api_kind == "GraphQL": elif api_kind == "GraphQL":
all_endpoints += _build_graphql_endpoints(base_ep, model, headers, operations) 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) all_endpoints += _build_restjson_endpoints(base_ep, model, headers, operations)
# Build URLs using {{base_url}} variable # Build URLs using {{base_url}} variable
@@ -595,7 +595,7 @@ def generate_collection(
if not ep["path"].startswith("http"): if not ep["path"].startswith("http"):
ep["url"] = f"{{{{base_url}}}}{ep['path']}" 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 { return {
"collection_name": name, "collection_name": name,

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent HTTP client engine.""" """APIClient - Agent - HTTP client engine."""
import re import re
import base64 import base64
from copy import deepcopy from copy import deepcopy
@@ -144,18 +144,18 @@ def send_request(req: HttpRequest, variables: dict = None) -> HttpResponse:
detail = str(e) detail = str(e)
if "CERTIFICATE_VERIFY_FAILED" in detail or "certificate" in detail.lower() or "SSL" in detail: if "CERTIFICATE_VERIFY_FAILED" in detail or "certificate" in detail.lower() or "SSL" in detail:
return HttpResponse(error=( 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"The server's certificate is not trusted or doesn't match the hostname.\n"
f"Tip: disable SSL verification in the request Settings tab." 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: except httpx.ConnectTimeout:
return HttpResponse(error=f"Connection timed out after {req.timeout}s") return HttpResponse(error=f"Connection timed out after {req.timeout}s")
except httpx.ReadTimeout: 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: except httpx.SSLError as e:
return HttpResponse(error=f"SSL error: {e}. Disable SSL verification if using a self-signed cert.") return HttpResponse(error=f"SSL error: {e}. Disable SSL verification if using a self-signed cert.")
except httpx.TooManyRedirects: 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: except Exception as e:
return HttpResponse(error=str(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 import threading
from http.server import BaseHTTPRequestHandler, HTTPServer 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, Parses OpenAPI 3.x and Swagger 2.0 specs (JSON or YAML) directly,
without needing AI tokens. without needing AI tokens.

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Storage layer (SQLite).""" """APIClient - Agent - Storage layer (SQLite)."""
import json import json
import sqlite3 import sqlite3
from pathlib import Path 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 dataclasses import dataclass, field
from typing import Optional 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 import re
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
@@ -124,7 +124,7 @@ class MessageBubble(QFrame):
self._text_lbl.setText(self._full_text) self._text_lbl.setText(self._full_text)
def finalize(self): 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: if self._finalized:
return return
self._finalized = True self._finalized = True

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent AI Assistant Dialog.""" """APIClient - Agent - AI Assistant Dialog."""
import json import json
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
@@ -43,7 +43,7 @@ class AnalysisWorker(QThread):
self.progress.emit("Checking for OpenAPI/Swagger spec…") self.progress.emit("Checking for OpenAPI/Swagger spec…")
spec = openapi_parser.detect_spec(content) spec = openapi_parser.detect_spec(content)
if spec: if spec:
self.progress.emit("OpenAPI spec detected parsing directly…") self.progress.emit("OpenAPI spec detected - parsing directly…")
result = openapi_parser.parse_spec(spec) result = openapi_parser.parse_spec(spec)
if self.base_url: if self.base_url:
result["base_url"] = self.base_url result["base_url"] = self.base_url
@@ -136,7 +136,7 @@ class AIAssistantDialog(QDialog):
layout.addWidget(footer) 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: 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.setFont(QFont("JetBrains Mono, Fira Code, Consolas", 10))
self.ek_preview.setPlaceholderText( self.ek_preview.setPlaceholderText(
"Fill in the form above and click Generate Collection to preview.\n\n" "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." "EKIKA Odoo API Framework documentation."
) )
self.ek_preview.setMaximumHeight(180) self.ek_preview.setMaximumHeight(180)
@@ -380,7 +380,7 @@ class AIAssistantDialog(QDialog):
self.ek_import_btn.setEnabled(True) self.ek_import_btn.setEnabled(True)
self.ek_env_btn.setEnabled(True) self.ek_env_btn.setEnabled(True)
self.ek_both_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): def _ekika_import(self):
if not self._result: if not self._result:
@@ -399,7 +399,7 @@ class AIAssistantDialog(QDialog):
self._do_create_env(self._result) 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: def _build_generic_tab(self) -> QWidget:
@@ -543,10 +543,10 @@ class AIAssistantDialog(QDialog):
lines = [ lines = [
f"✓ Parsed via: {src_label}", f"✓ Parsed via: {src_label}",
f"✓ Collection: {result.get('collection_name', 'Unnamed')}", 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"✓ Auth type: {result.get('auth_type', 'none')}",
f"✓ Endpoints: {len(endpoints)} found", f"✓ Endpoints: {len(endpoints)} found",
f"✓ Env vars: {list(env_vars.keys()) or ''}", f"✓ Env vars: {list(env_vars.keys()) or '-'}",
] ]
if notes: if notes:
lines += ["", "── Notes ─────────────────", notes] lines += ["", "── Notes ─────────────────", notes]
@@ -565,7 +565,7 @@ class AIAssistantDialog(QDialog):
self.analyze_btn.setEnabled(True) self.analyze_btn.setEnabled(True)
self.progress_bar.setVisible(False) self.progress_bar.setVisible(False)
self.result_view.setPlainText(f"✗ Error:\n\n{msg}") 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): def _set_generic_action_buttons(self, enabled: bool):
self.import_btn.setEnabled(enabled) self.import_btn.setEnabled(enabled)
@@ -586,7 +586,7 @@ class AIAssistantDialog(QDialog):
self._do_create_env(self._generic_result) self._do_create_env(self._generic_result)
# ══════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════
# Tab 3 Settings # Tab 3 - Settings
# ══════════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════════
def _build_settings_tab(self) -> QWidget: def _build_settings_tab(self) -> QWidget:
@@ -598,7 +598,7 @@ class AIAssistantDialog(QDialog):
hint = QLabel( hint = QLabel(
"EKIKA AI Assistant uses Claude by Anthropic to analyze plain-text API documentation.\n" "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." "no API key required for those."
) )
hint.setObjectName("hintText") hint.setObjectName("hintText")
@@ -683,7 +683,7 @@ class AIAssistantDialog(QDialog):
def _do_create_env(self, result: dict): def _do_create_env(self, result: dict):
env_vars = result.get("environment_variables", {}) env_vars = result.get("environment_variables", {})
col_name = result.get("collection_name", "AI Import") col_name = result.get("collection_name", "AI Import")
env_name = f"{col_name} Environment" env_name = f"{col_name} - Environment"
if not env_vars: if not env_vars:
QMessageBox.information(self, "No Variables", "No environment variables detected.") 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 ( from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QComboBox, QDialog, QVBoxLayout, QHBoxLayout, QComboBox,
QTextEdit, QPushButton, QLabel, QApplication, QWidget 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 ( from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QTreeWidget, QTreeWidgetItem, QComboBox, QHeaderView, QProgressBar, QWidget QTreeWidget, QTreeWidgetItem, QComboBox, QHeaderView, QProgressBar, QWidget
@@ -168,7 +168,7 @@ class CollectionRunnerDialog(QDialog):
status_str = str(result.status) status_str = str(result.status)
row_color = Colors.SUCCESS if result.status < 400 else Colors.ERROR 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([ item = QTreeWidgetItem([
f"{result.method} {result.request_name}", f"{result.method} {result.request_name}",
status_str, status_str,
@@ -189,6 +189,6 @@ class CollectionRunnerDialog(QDialog):
def _on_finished(self): def _on_finished(self):
self.run_btn.setEnabled(True) self.run_btn.setEnabled(True)
self.summary_label.setText( 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" 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 ( from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem, QDialog, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem,
QPushButton, QTableWidget, QTableWidgetItem, QHeaderView, QPushButton, QTableWidget, QTableWidgetItem, QHeaderView,

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Import Dialog.""" """APIClient - Agent - Import Dialog."""
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget, QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget,
QTextEdit, QPushButton, QLabel, QFileDialog, QMessageBox QTextEdit, QPushButton, QLabel, QFileDialog, QMessageBox
@@ -76,7 +76,7 @@ class ImportDialog(QDialog):
layout.setContentsMargins(16, 12, 16, 12) layout.setContentsMargins(16, 12, 16, 12)
layout.setSpacing(8) 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") hint.setObjectName("hintText")
layout.addWidget(hint) layout.addWidget(hint)

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Request Panel.""" """APIClient - Agent - Request Panel."""
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QComboBox, QLineEdit, QWidget, QVBoxLayout, QHBoxLayout, QComboBox, QLineEdit,
QPushButton, QTabWidget, QTableWidget, QTableWidgetItem, QPushButton, QTabWidget, QTableWidget, QTableWidgetItem,
@@ -227,7 +227,7 @@ class RequestPanel(QWidget):
self.url_input = QLineEdit() self.url_input = QLineEdit()
self.url_input.setObjectName("urlBar") 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.url_input.returnPressed.connect(self._send)
self.send_btn = QPushButton("Send") self.send_btn = QPushButton("Send")
@@ -359,7 +359,7 @@ class RequestPanel(QWidget):
# ── Slots ──────────────────────────────────────────────────────────────── # ── Slots ────────────────────────────────────────────────────────────────
def _on_method_changed(self, method: str): 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) color = method_color(method)
self.method_combo.setStyleSheet(f"QComboBox#methodCombo {{ color: {color}; }}") self.method_combo.setStyleSheet(f"QComboBox#methodCombo {{ color: {color}; }}")
@@ -377,7 +377,7 @@ class RequestPanel(QWidget):
parsed = json.loads(text) parsed = json.loads(text)
self.body_editor.setPlainText(json.dumps(parsed, indent=2, ensure_ascii=False)) self.body_editor.setPlainText(json.dumps(parsed, indent=2, ensure_ascii=False))
except json.JSONDecodeError: except json.JSONDecodeError:
pass # not valid JSON leave as-is pass # not valid JSON - leave as-is
def _send(self): def _send(self):
self.send_requested.emit(self._build_request()) self.send_requested.emit(self._build_request())

View File

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

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Collections Sidebar.""" """APIClient - Agent - Collections Sidebar."""
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QWidget, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem,
QPushButton, QInputDialog, QMenu, QLineEdit, QLabel, QMessageBox 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.QtWidgets import QWidget, QVBoxLayout, QTabWidget, QPushButton, QTabBar
from PyQt6.QtCore import pyqtSignal, Qt 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. 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. Only truly dynamic values (per-request method color, status badge) stay inline.
""" """
from PyQt6.QtGui import QColor, QPalette 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 asyncio
import queue import queue
import time import time