Files
APIClient-Agent/app/core/openapi_parser.py
Anand Shukla 01662f7e0e Initial release — APIClient - Agent v2.0.0
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>
2026-03-28 17:38:57 +05:30

237 lines
7.5 KiB
Python

"""APIClient - Agent — OpenAPI / Swagger spec parser.
Parses OpenAPI 3.x and Swagger 2.0 specs (JSON or YAML) directly,
without needing AI tokens.
"""
import json
import re
def _try_yaml(text: str) -> dict | None:
try:
import yaml
return yaml.safe_load(text)
except Exception:
return None
def _try_json(text: str) -> dict | None:
try:
return json.loads(text)
except Exception:
return None
def detect_spec(text: str) -> dict | None:
"""Try to parse text as OpenAPI/Swagger JSON or YAML. Returns raw dict or None."""
data = _try_json(text) or _try_yaml(text)
if not isinstance(data, dict):
return None
if "openapi" in data or "swagger" in data:
return data
return None
def parse_spec(data: dict) -> dict:
"""
Parse an OpenAPI 3.x or Swagger 2.0 spec dict into EKIKA's internal format:
{
"collection_name": str,
"base_url": str,
"auth_type": str,
"endpoints": [...],
"environment_variables": {...}
}
"""
version = str(data.get("openapi", data.get("swagger", "2")))
is_v3 = version.startswith("3")
# Collection name
info = data.get("info", {})
collection_name = info.get("title", "Imported API")
# Base URL
if is_v3:
servers = data.get("servers", [])
base_url = servers[0].get("url", "") if servers else ""
else:
host = data.get("host", "")
schemes = data.get("schemes", ["https"])
base_p = data.get("basePath", "/")
base_url = f"{schemes[0]}://{host}{base_p}" if host else ""
# Clean trailing slash
base_url = base_url.rstrip("/")
# Auth detection
security_schemes = {}
if is_v3:
security_schemes = data.get("components", {}).get("securitySchemes", {})
else:
security_schemes = data.get("securityDefinitions", {})
auth_type = "none"
for scheme in security_schemes.values():
t = scheme.get("type", "").lower()
if t in ("http", "bearer") or scheme.get("scheme", "").lower() == "bearer":
auth_type = "bearer"
break
if t == "apikey":
auth_type = "apikey"
break
if t in ("basic", "http") and scheme.get("scheme", "").lower() == "basic":
auth_type = "basic"
break
# Endpoints
endpoints = []
paths = data.get("paths", {})
for path, path_item in paths.items():
if not isinstance(path_item, dict):
continue
for method in ("get", "post", "put", "patch", "delete", "head", "options"):
op = path_item.get(method)
if not isinstance(op, dict):
continue
name = op.get("summary") or op.get("operationId") or f"{method.upper()} {path}"
description = op.get("description", "")
# Headers
headers: dict = {}
# Query params
params: dict = {}
body_example = ""
content_type = "application/json"
body_type = "raw"
# Parameters
for param in op.get("parameters", []):
if not isinstance(param, dict):
continue
p_in = param.get("in", "")
p_name = param.get("name", "")
if p_in == "query":
params[p_name] = param.get("example", "")
elif p_in == "header":
headers[p_name] = param.get("example", "")
# Request body (OpenAPI 3)
if is_v3 and "requestBody" in op:
rb = op["requestBody"]
content = rb.get("content", {})
if "application/json" in content:
schema = content["application/json"].get("schema", {})
body_example = _schema_to_example_str(schema)
content_type = "application/json"
elif "application/x-www-form-urlencoded" in content:
body_type = "form-urlencoded"
content_type = ""
elif content:
first_ct = next(iter(content))
content_type = first_ct
# Request body (Swagger 2)
if not is_v3:
consumes = op.get("consumes", data.get("consumes", ["application/json"]))
for param in op.get("parameters", []):
if param.get("in") == "body":
schema = param.get("schema", {})
body_example = _schema_to_example_str(schema)
if consumes:
content_type = consumes[0]
# Add auth header hint
if auth_type == "bearer":
headers.setdefault("Authorization", "Bearer {{token}}")
elif auth_type == "apikey":
headers.setdefault("X-API-Key", "{{api_key}}")
# Basic test script
test_script = (
f"pm.test('Status OK', lambda: pm.response.to_have_status(200))\n"
f"pm.test('Has body', lambda: expect(pm.response.text).to_be_truthy())"
)
endpoints.append({
"name": name,
"method": method.upper(),
"path": path,
"description": description,
"headers": headers,
"params": params,
"body": body_example,
"body_type": body_type,
"content_type": content_type,
"test_script": test_script,
})
# Environment variables
env_vars: dict = {}
if base_url:
env_vars["base_url"] = base_url
if auth_type == "bearer":
env_vars["token"] = ""
elif auth_type == "apikey":
env_vars["api_key"] = ""
elif auth_type == "basic":
env_vars["username"] = ""
env_vars["password"] = ""
return {
"collection_name": collection_name,
"base_url": base_url,
"auth_type": auth_type,
"endpoints": endpoints,
"environment_variables": env_vars,
}
def _schema_to_example_str(schema: dict) -> str:
"""Generate a compact JSON example string from an OpenAPI schema."""
try:
example = _schema_to_example(schema)
return json.dumps(example, indent=2, ensure_ascii=False)
except Exception:
return ""
def _schema_to_example(schema: dict, depth: int = 0) -> object:
if depth > 5:
return {}
if not isinstance(schema, dict):
return {}
# Use provided example first
if "example" in schema:
return schema["example"]
if "default" in schema:
return schema["default"]
t = schema.get("type", "object")
if t == "object" or "properties" in schema:
result = {}
for k, v in schema.get("properties", {}).items():
result[k] = _schema_to_example(v, depth + 1)
return result
if t == "array":
items = schema.get("items", {})
return [_schema_to_example(items, depth + 1)]
if t == "string":
fmt = schema.get("format", "")
if fmt == "date-time": return "2024-01-01T00:00:00Z"
if fmt == "date": return "2024-01-01"
if fmt == "email": return "user@example.com"
if fmt == "uuid": return "00000000-0000-0000-0000-000000000000"
return schema.get("enum", ["string"])[0]
if t == "integer": return 0
if t == "number": return 0.0
if t == "boolean": return True
return {}