Update documentation.
This commit is contained in:
74
README.md
74
README.md
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user