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>
104 lines
3.5 KiB
Python
104 lines
3.5 KiB
Python
"""Import from Postman Collection v2.1 JSON or curl command."""
|
|
import json
|
|
import re
|
|
import shlex
|
|
from app.models import HttpRequest
|
|
|
|
|
|
def from_postman_collection(json_text: str) -> tuple[str, list[HttpRequest]]:
|
|
"""Returns (collection_name, list of HttpRequest)."""
|
|
data = json.loads(json_text)
|
|
name = data.get("info", {}).get("name", "Imported Collection")
|
|
requests = []
|
|
|
|
def parse_item(item):
|
|
if "request" in item:
|
|
r = item["request"]
|
|
method = r.get("method", "GET")
|
|
url_obj = r.get("url", {})
|
|
if isinstance(url_obj, str):
|
|
url = url_obj
|
|
params = {}
|
|
else:
|
|
raw = url_obj.get("raw", "")
|
|
url = raw.split("?")[0] if "?" in raw else raw
|
|
params = {}
|
|
for qp in url_obj.get("query", []):
|
|
if not qp.get("disabled"):
|
|
params[qp.get("key", "")] = qp.get("value", "")
|
|
|
|
headers = {}
|
|
for h in r.get("header", []):
|
|
if not h.get("disabled"):
|
|
headers[h.get("key", "")] = h.get("value", "")
|
|
|
|
body_obj = r.get("body", {})
|
|
body = ""
|
|
body_type = "raw"
|
|
if body_obj:
|
|
mode = body_obj.get("mode", "raw")
|
|
if mode == "raw":
|
|
body = body_obj.get("raw", "")
|
|
body_type = "raw"
|
|
elif mode == "urlencoded":
|
|
pairs = body_obj.get("urlencoded", [])
|
|
body = "&".join(f"{p['key']}={p.get('value','')}" for p in pairs if not p.get("disabled"))
|
|
body_type = "urlencoded"
|
|
|
|
requests.append(HttpRequest(
|
|
method=method, url=url, headers=headers,
|
|
params=params, body=body, body_type=body_type,
|
|
name=item.get("name", "")
|
|
))
|
|
elif "item" in item:
|
|
for sub in item["item"]:
|
|
parse_item(sub)
|
|
|
|
for item in data.get("item", []):
|
|
parse_item(item)
|
|
|
|
return name, requests
|
|
|
|
|
|
def from_curl(curl_cmd: str) -> HttpRequest:
|
|
"""Parse a curl command string into an HttpRequest."""
|
|
# Normalize line continuations
|
|
cmd = curl_cmd.replace("\\\n", " ").strip()
|
|
try:
|
|
tokens = shlex.split(cmd)
|
|
except ValueError:
|
|
tokens = cmd.split()
|
|
|
|
req = HttpRequest(method="GET")
|
|
i = 1 # skip 'curl'
|
|
while i < len(tokens):
|
|
token = tokens[i]
|
|
if token in ("-X", "--request") and i + 1 < len(tokens):
|
|
req.method = tokens[i + 1].upper()
|
|
i += 2
|
|
elif token in ("-H", "--header") and i + 1 < len(tokens):
|
|
header = tokens[i + 1]
|
|
if ":" in header:
|
|
k, _, v = header.partition(":")
|
|
req.headers[k.strip()] = v.strip()
|
|
i += 2
|
|
elif token in ("-d", "--data", "--data-raw", "--data-binary") and i + 1 < len(tokens):
|
|
req.body = tokens[i + 1]
|
|
if req.method == "GET":
|
|
req.method = "POST"
|
|
i += 2
|
|
elif token in ("-u", "--user") and i + 1 < len(tokens):
|
|
user_pass = tokens[i + 1]
|
|
if ":" in user_pass:
|
|
u, _, p = user_pass.partition(":")
|
|
req.auth_type = "basic"
|
|
req.auth_data = {"username": u, "password": p}
|
|
i += 2
|
|
elif not token.startswith("-") and not req.url:
|
|
req.url = token.strip("'\"")
|
|
i += 1
|
|
else:
|
|
i += 1
|
|
|
|
return req
|