Update documentation.
This commit is contained in:
74
README.md
74
README.md
@@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
|
||||
---
|
||||
@@ -8,31 +8,31 @@
|
||||
## Features
|
||||
|
||||
### Core API Testing
|
||||
- **Multi-tab request editor** — work on multiple requests simultaneously, drag to reorder
|
||||
- **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
|
||||
- **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)
|
||||
- **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
|
||||
- **Response viewer** — syntax-highlighted body, headers table, test results, search, copy, save
|
||||
- **WebSocket client** — connect, send, receive, log messages
|
||||
- **Mock server** — local HTTP mock with configurable routes
|
||||
- **Multi-tab request editor** - work on multiple requests simultaneously, drag to reorder
|
||||
- **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
|
||||
- **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)
|
||||
- **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
|
||||
- **Response viewer** - syntax-highlighted body, headers table, test results, search, copy, save
|
||||
- **WebSocket client** - connect, send, receive, log messages
|
||||
- **Mock server** - local HTTP mock with configurable routes
|
||||
|
||||
### Collections & Environments
|
||||
- **Collections sidebar** — import/export Postman Collection v2.1 JSON, cURL
|
||||
- **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
|
||||
- **History** — every sent request automatically saved
|
||||
- **Collections sidebar** - import/export Postman Collection v2.1 JSON, cURL
|
||||
- **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
|
||||
- **History** - every sent request automatically saved
|
||||
|
||||
### AI Co-pilot (Claude-powered)
|
||||
- **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
|
||||
- **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
|
||||
- **Multi-turn conversation** — full history maintained per session; Clear to reset
|
||||
- **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
|
||||
- **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
|
||||
- **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
|
||||
- **Multi-turn conversation** - full history maintained per session; Clear to reset
|
||||
- **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 API Framework specialisation
|
||||
- Generates full CRUD + Execute / Export / Report / Fields / Access-Rights endpoints per model
|
||||
@@ -73,14 +73,14 @@ pyinstaller>=6.0.0
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1 — Send your first request
|
||||
### 1 - Send your first request
|
||||
|
||||
1. Launch the app: `python main.py`
|
||||
2. Type a URL in the bar, e.g. `https://jsonplaceholder.typicode.com/todos/1`
|
||||
3. Press **Send** (or `Ctrl+Enter`)
|
||||
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`
|
||||
2. Add variables:
|
||||
@@ -93,7 +93,7 @@ pyinstaller>=6.0.0
|
||||
5. In Headers, add `Authorization: Bearer {{api_key}}`
|
||||
6. Variables are resolved automatically at send time
|
||||
|
||||
### 3 — Import a collection
|
||||
### 3 - Import a collection
|
||||
|
||||
**From Postman export:**
|
||||
1. `File → Import…`
|
||||
@@ -109,11 +109,11 @@ curl -X POST https://api.example.com/v1/orders \
|
||||
|
||||
**From OpenAPI spec:**
|
||||
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
|
||||
|
||||
@@ -131,8 +131,8 @@ curl -X POST https://api.example.com/v1/orders \
|
||||
| Models | `sale.order, res.partner, account.move` |
|
||||
| Operations | ✓ List, Get, Create, Update, Delete |
|
||||
|
||||
4. Click **Generate Collection** — preview appears instantly
|
||||
5. Click **Import Both** — collection + environment are saved
|
||||
4. Click **Generate Collection** - preview appears instantly
|
||||
5. Click **Import Both** - collection + environment are saved
|
||||
|
||||
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.
|
||||
|
||||
@@ -227,7 +227,7 @@ AI: A 401 on the EKIKA JSON-API endpoint means the x-api-key header is
|
||||
[ 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
|
||||
|
||||
@@ -305,8 +305,8 @@ APIClient-Agent/
|
||||
│ ├── core/
|
||||
│ │ ├── storage.py # SQLite persistence (collections, environments, history)
|
||||
│ │ ├── http_client.py # httpx-based request engine, variable resolution
|
||||
│ │ ├── ai_client.py # Claude API — collection generation from docs
|
||||
│ │ ├── ai_chat.py # Claude API — multi-turn conversational co-pilot
|
||||
│ │ ├── ai_client.py # Claude API - collection generation from docs
|
||||
│ │ ├── ai_chat.py # Claude API - multi-turn conversational co-pilot
|
||||
│ │ ├── openapi_parser.py # OpenAPI 3.x / Swagger 2.0 local parser
|
||||
│ │ ├── ekika_odoo_generator.py# EKIKA Odoo framework collection generator
|
||||
│ │ ├── 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`
|
||||
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:
|
||||
|
||||
```
|
||||
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.
|
||||
```
|
||||
|
||||
@@ -382,10 +382,10 @@ The executable is produced in `dist/APIClient-Agent`.
|
||||
3. Commit your changes: `git commit -m "Add my feature"`
|
||||
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
|
||||
|
||||
[MIT License](LICENSE) — Copyright (c) 2026 EKIKA.co
|
||||
[MIT License](LICENSE) - Copyright (c) 2026 EKIKA.co
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""APIClient - Agent — Code snippet generators."""
|
||||
"""APIClient - Agent - Code snippet generators."""
|
||||
import json
|
||||
from urllib.parse import urlencode
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""APIClient - Agent — Lightweight HTTP mock server."""
|
||||
"""APIClient - Agent - Lightweight HTTP mock server."""
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""APIClient - Agent — Storage layer (SQLite)."""
|
||||
"""APIClient - Agent - Storage layer (SQLite)."""
|
||||
import json
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""APIClient - Agent — Core data models."""
|
||||
"""APIClient - Agent - Core data models."""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""APIClient - Agent — WebSocket client panel."""
|
||||
"""APIClient - Agent - WebSocket client panel."""
|
||||
import asyncio
|
||||
import queue
|
||||
import time
|
||||
|
||||
Reference in New Issue
Block a user