diff --git a/README.md b/README.md index e30a098..8d0a6e4 100644 --- a/README.md +++ b/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 diff --git a/app/core/ai_chat.py b/app/core/ai_chat.py index 4acbdf2..4627f5a 100644 --- a/app/core/ai_chat.py +++ b/app/core/ai_chat.py @@ -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 diff --git a/app/core/ai_client.py b/app/core/ai_client.py index 3a87a84..df52881 100644 --- a/app/core/ai_client.py +++ b/app/core/ai_client.py @@ -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) diff --git a/app/core/code_gen.py b/app/core/code_gen.py index 4685863..4c07c79 100644 --- a/app/core/code_gen.py +++ b/app/core/code_gen.py @@ -1,4 +1,4 @@ -"""APIClient - Agent — Code snippet generators.""" +"""APIClient - Agent - Code snippet generators.""" import json from urllib.parse import urlencode diff --git a/app/core/ekika_odoo_generator.py b/app/core/ekika_odoo_generator.py index 8bedca5..d1b6053 100644 --- a/app/core/ekika_odoo_generator.py +++ b/app/core/ekika_odoo_generator.py @@ -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, diff --git a/app/core/http_client.py b/app/core/http_client.py index 8841f7b..dd8ad42 100644 --- a/app/core/http_client.py +++ b/app/core/http_client.py @@ -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)) diff --git a/app/core/mock_server.py b/app/core/mock_server.py index b648338..494ba6a 100644 --- a/app/core/mock_server.py +++ b/app/core/mock_server.py @@ -1,4 +1,4 @@ -"""APIClient - Agent — Lightweight HTTP mock server.""" +"""APIClient - Agent - Lightweight HTTP mock server.""" import threading from http.server import BaseHTTPRequestHandler, HTTPServer diff --git a/app/core/openapi_parser.py b/app/core/openapi_parser.py index d1f378b..ea5782b 100644 --- a/app/core/openapi_parser.py +++ b/app/core/openapi_parser.py @@ -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. diff --git a/app/core/storage.py b/app/core/storage.py index 80b06a3..bae1bb4 100644 --- a/app/core/storage.py +++ b/app/core/storage.py @@ -1,4 +1,4 @@ -"""APIClient - Agent — Storage layer (SQLite).""" +"""APIClient - Agent - Storage layer (SQLite).""" import json import sqlite3 from pathlib import Path diff --git a/app/models.py b/app/models.py index 34fd776..8309762 100644 --- a/app/models.py +++ b/app/models.py @@ -1,4 +1,4 @@ -"""APIClient - Agent — Core data models.""" +"""APIClient - Agent - Core data models.""" from dataclasses import dataclass, field from typing import Optional diff --git a/app/ui/ai_chat_panel.py b/app/ui/ai_chat_panel.py index 604006c..11ef336 100644 --- a/app/ui/ai_chat_panel.py +++ b/app/ui/ai_chat_panel.py @@ -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 diff --git a/app/ui/ai_panel.py b/app/ui/ai_panel.py index ccaa006..85267c1 100644 --- a/app/ui/ai_panel.py +++ b/app/ui/ai_panel.py @@ -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.") diff --git a/app/ui/code_gen_dialog.py b/app/ui/code_gen_dialog.py index 1a95ffb..aae7240 100644 --- a/app/ui/code_gen_dialog.py +++ b/app/ui/code_gen_dialog.py @@ -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 diff --git a/app/ui/collection_runner.py b/app/ui/collection_runner.py index 2648f12..5829051 100644 --- a/app/ui/collection_runner.py +++ b/app/ui/collection_runner.py @@ -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" ) diff --git a/app/ui/environment_dialog.py b/app/ui/environment_dialog.py index cf2d3d9..68e6e44 100644 --- a/app/ui/environment_dialog.py +++ b/app/ui/environment_dialog.py @@ -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, diff --git a/app/ui/import_dialog.py b/app/ui/import_dialog.py index 4631504..bd10824 100644 --- a/app/ui/import_dialog.py +++ b/app/ui/import_dialog.py @@ -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) diff --git a/app/ui/main_window.py b/app/ui/main_window.py index 91fb07c..5efcf3d 100644 --- a/app/ui/main_window.py +++ b/app/ui/main_window.py @@ -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() diff --git a/app/ui/mock_server_panel.py b/app/ui/mock_server_panel.py index 759e0c7..d6a04f6 100644 --- a/app/ui/mock_server_panel.py +++ b/app/ui/mock_server_panel.py @@ -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, diff --git a/app/ui/request_panel.py b/app/ui/request_panel.py index 43a2a77..231049d 100644 --- a/app/ui/request_panel.py +++ b/app/ui/request_panel.py @@ -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()) diff --git a/app/ui/response_panel.py b/app/ui/response_panel.py index 45b9a00..d656ad3 100644 --- a/app/ui/response_panel.py +++ b/app/ui/response_panel.py @@ -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)) diff --git a/app/ui/search_dialog.py b/app/ui/search_dialog.py index d124f01..e2d6262 100644 --- a/app/ui/search_dialog.py +++ b/app/ui/search_dialog.py @@ -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 diff --git a/app/ui/sidebar.py b/app/ui/sidebar.py index f682329..ec3a944 100644 --- a/app/ui/sidebar.py +++ b/app/ui/sidebar.py @@ -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 diff --git a/app/ui/tabs_manager.py b/app/ui/tabs_manager.py index 396e684..7c38e6c 100644 --- a/app/ui/tabs_manager.py +++ b/app/ui/tabs_manager.py @@ -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 diff --git a/app/ui/theme.py b/app/ui/theme.py index 45ddbcb..24eddd0 100644 --- a/app/ui/theme.py +++ b/app/ui/theme.py @@ -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 diff --git a/app/ui/websocket_panel.py b/app/ui/websocket_panel.py index 5dd4dd1..1149c97 100644 --- a/app/ui/websocket_panel.py +++ b/app/ui/websocket_panel.py @@ -1,4 +1,4 @@ -"""APIClient - Agent — WebSocket client panel.""" +"""APIClient - Agent - WebSocket client panel.""" import asyncio import queue import time