AI-first API testing desktop client built with Python + PyQt6. Features: - Multi-tab HTTP request editor with params/headers/body/auth/tests - KeyValueTable with per-row enable/disable checkboxes and 36px rows - Format JSON button, syntax highlighting, pre-request & test scripts - Collections, environments, history, import/export (Postman v2.1, cURL) - OpenAPI 3.x / Swagger 2.0 local parser (no AI tokens) - EKIKA Odoo API Framework generator — JSON-API, REST JSON, GraphQL, Custom REST JSON with all auth types (instant, no AI tokens) - Persistent AI chat sidebar (Claude-powered co-pilot) with streaming, context-aware suggestions, and one-click Apply to request editor - AI collection generator from any docs URL or pasted spec - WebSocket client, Mock server, Collection runner, Code generator - Dark/light theme engine (global QSS, object-name selectors) - SSL error detection with actionable hints - MIT License Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
164 lines
5.1 KiB
Python
164 lines
5.1 KiB
Python
"""APIClient - Agent — Code snippet generators."""
|
|
import json
|
|
from urllib.parse import urlencode
|
|
|
|
from app.models import HttpRequest
|
|
|
|
|
|
def _qs(req: HttpRequest) -> str:
|
|
if not req.params:
|
|
return req.url
|
|
sep = "&" if "?" in req.url else "?"
|
|
return req.url + sep + urlencode(req.params)
|
|
|
|
|
|
def to_curl(req: HttpRequest) -> str:
|
|
parts = [f"curl -X {req.method}"]
|
|
parts.append(f' "{_qs(req)}"')
|
|
for k, v in req.headers.items():
|
|
parts.append(f" -H '{k}: {v}'")
|
|
if req.body:
|
|
escaped = req.body.replace("'", r"'\''")
|
|
parts.append(f" -d '{escaped}'")
|
|
if not req.ssl_verify:
|
|
parts.append(" --insecure")
|
|
return " \\\n".join(parts)
|
|
|
|
|
|
def to_python_requests(req: HttpRequest) -> str:
|
|
headers = dict(req.headers)
|
|
if req.body and "Content-Type" not in headers:
|
|
headers["Content-Type"] = "application/json"
|
|
|
|
lines = ["import requests", ""]
|
|
lines.append(f'url = "{req.url}"')
|
|
if headers:
|
|
lines.append(f"headers = {json.dumps(headers, indent=4)}")
|
|
if req.params:
|
|
lines.append(f"params = {json.dumps(req.params, indent=4)}")
|
|
lines.append("")
|
|
|
|
call_args = ["url"]
|
|
if headers:
|
|
call_args.append("headers=headers")
|
|
if req.params:
|
|
call_args.append("params=params")
|
|
if req.body:
|
|
lines.append(f"payload = {json.dumps(req.body)}")
|
|
call_args.append("data=payload")
|
|
if not req.ssl_verify:
|
|
call_args.append("verify=False")
|
|
|
|
lines.append(f"response = requests.{req.method.lower()}({', '.join(call_args)})")
|
|
lines.append("")
|
|
lines.append("print(response.status_code)")
|
|
lines.append("print(response.json())")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def to_python_httpx(req: HttpRequest) -> str:
|
|
headers = dict(req.headers)
|
|
lines = ["import httpx", ""]
|
|
lines.append(f'url = "{req.url}"')
|
|
if headers:
|
|
lines.append(f"headers = {json.dumps(headers, indent=4)}")
|
|
if req.params:
|
|
lines.append(f"params = {json.dumps(req.params, indent=4)}")
|
|
lines.append("")
|
|
|
|
call_args = ["url"]
|
|
if headers:
|
|
call_args.append("headers=headers")
|
|
if req.params:
|
|
call_args.append("params=params")
|
|
if req.body:
|
|
lines.append(f"payload = {json.dumps(req.body)}")
|
|
call_args.append("content=payload.encode()")
|
|
if not req.ssl_verify:
|
|
call_args.append("verify=False")
|
|
|
|
lines.append("with httpx.Client() as client:")
|
|
lines.append(f" response = client.{req.method.lower()}({', '.join(call_args)})")
|
|
lines.append(" print(response.status_code)")
|
|
lines.append(" print(response.json())")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def to_javascript_fetch(req: HttpRequest) -> str:
|
|
options: dict = {"method": req.method}
|
|
if req.headers:
|
|
options["headers"] = req.headers
|
|
if req.body:
|
|
options["body"] = req.body
|
|
|
|
lines = [
|
|
f'const response = await fetch("{_qs(req)}", {json.dumps(options, indent=2)});',
|
|
"",
|
|
"const data = await response.json();",
|
|
"console.log(response.status, data);",
|
|
]
|
|
return "\n".join(lines)
|
|
|
|
|
|
def to_javascript_axios(req: HttpRequest) -> str:
|
|
config: dict = {}
|
|
if req.headers:
|
|
config["headers"] = req.headers
|
|
if req.params:
|
|
config["params"] = req.params
|
|
if req.body:
|
|
config["data"] = req.body
|
|
|
|
lines = ["const axios = require('axios');", ""]
|
|
if config:
|
|
lines.append(f"const config = {json.dumps(config, indent=2)};")
|
|
lines.append("")
|
|
lines.append(f'const response = await axios.{req.method.lower()}("{req.url}", config);')
|
|
else:
|
|
lines.append(f'const response = await axios.{req.method.lower()}("{req.url}");')
|
|
lines.append("console.log(response.status, response.data);")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def to_ruby(req: HttpRequest) -> str:
|
|
lines = [
|
|
"require 'net/http'",
|
|
"require 'uri'",
|
|
"require 'json'",
|
|
"",
|
|
f'uri = URI.parse("{_qs(req)}")',
|
|
f"http = Net::HTTP.new(uri.host, uri.port)",
|
|
"http.use_ssl = uri.scheme == 'https'",
|
|
]
|
|
if not req.ssl_verify:
|
|
lines.append("http.verify_mode = OpenSSL::SSL::VERIFY_NONE")
|
|
lines.append("")
|
|
klass_map = {
|
|
"GET": "Net::HTTP::Get", "POST": "Net::HTTP::Post",
|
|
"PUT": "Net::HTTP::Put", "PATCH": "Net::HTTP::Patch",
|
|
"DELETE": "Net::HTTP::Delete", "HEAD": "Net::HTTP::Head",
|
|
}
|
|
klass = klass_map.get(req.method, "Net::HTTP::Get")
|
|
lines.append(f"request = {klass}.new(uri.request_uri)")
|
|
for k, v in req.headers.items():
|
|
lines.append(f'request["{k}"] = "{v}"')
|
|
if req.body:
|
|
lines.append(f"request.body = {json.dumps(req.body)}")
|
|
lines += [
|
|
"",
|
|
"response = http.request(request)",
|
|
"puts response.code",
|
|
"puts response.body",
|
|
]
|
|
return "\n".join(lines)
|
|
|
|
|
|
GENERATORS = {
|
|
"curl": to_curl,
|
|
"Python (requests)": to_python_requests,
|
|
"Python (httpx)": to_python_httpx,
|
|
"JavaScript (fetch)": to_javascript_fetch,
|
|
"JavaScript (axios)": to_javascript_axios,
|
|
"Ruby (Net::HTTP)": to_ruby,
|
|
}
|