Files
APIClient-Agent/app/core/ekika_odoo_generator.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

614 lines
22 KiB
Python

"""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,
and special operations are derived directly from the framework documentation.
Supported API kinds: JSON-API, REST JSON, GraphQL, Custom REST JSON
Supported auth types: API Key, Basic Auth, User Credentials, OAuth2, JWT, Public
"""
import json
import re
# ── Constants ─────────────────────────────────────────────────────────────────
API_KINDS = ["JSON-API", "REST JSON", "GraphQL", "Custom REST JSON"]
AUTH_TYPES = [
"API Key",
"Basic Auth",
"User Credentials",
"OAuth2",
"JWT",
"Public",
]
OPERATIONS = [
"List Records",
"Get Single Record",
"Create Record",
"Update Record",
"Delete Record",
"Execute Method",
"Export to Excel",
"Generate Report",
"Get Fields",
"Check Access Rights",
]
# Content-Type per API kind
_CT = {
"JSON-API": "application/vnd.api+json",
"REST JSON": "application/json",
"GraphQL": "application/json",
"Custom REST JSON": "application/json",
}
# ── Auth header builder ───────────────────────────────────────────────────────
def _auth_headers(auth_type: str) -> dict:
if auth_type == "API Key":
return {"x-api-key": "{{api_key}}"}
if auth_type == "Basic Auth":
return {"Authorization": "Basic {{base64_user_pass}}"}
if auth_type == "User Credentials":
return {"username": "{{username}}", "password": "{{password}}"}
if auth_type == "OAuth2":
return {"Authorization": "Bearer {{access_token}}"}
if auth_type == "JWT":
return {"Authorization": "Bearer {{jwt_token}}"}
return {} # Public
def _env_vars(instance_url: str, auth_type: str, extra: dict = None) -> dict:
base = instance_url.rstrip("/")
env: dict = {"base_url": base}
if auth_type == "API Key":
env["api_key"] = extra.get("api_key", "") if extra else ""
elif auth_type == "Basic Auth":
env["base64_user_pass"] = extra.get("base64_user_pass", "") if extra else ""
env["username"] = extra.get("username", "") if extra else ""
env["password"] = extra.get("password", "") if extra else ""
elif auth_type == "User Credentials":
env["username"] = extra.get("username", "") if extra else ""
env["password"] = extra.get("password", "") if extra else ""
elif auth_type == "OAuth2":
env["access_token"] = extra.get("access_token", "") if extra else ""
env["refresh_token"] = ""
env["client_id"] = ""
env["client_secret"] = ""
elif auth_type == "JWT":
env["jwt_token"] = extra.get("jwt_token", "") if extra else ""
return env
def _clean_endpoint(endpoint: str) -> str:
"""Normalise endpoint slug — ensure leading slash, strip trailing slash."""
ep = endpoint.strip().strip("/")
return f"/{ep}" if ep else "/api"
def _model_gql(model: str) -> str:
"""Convert 'sale.order''sale_order' for GraphQL field names."""
return model.replace(".", "_")
# ── Request builders ──────────────────────────────────────────────────────────
def _jsonapi_test(status: int = 200) -> str:
return (
f"pm.test('Status {status}', lambda: pm.response.to_have_status({status}))\n"
f"pm.test('Has data', lambda: expect(pm.response.json()).to_have_key('data'))"
)
def _rest_test(status: int = 200) -> str:
return (
f"pm.test('Status {status}', lambda: pm.response.to_have_status({status}))\n"
f"pm.test('Has body', lambda: expect(pm.response.text).to_be_truthy())"
)
def _build_jsonapi_endpoints(base_ep: str, model: str, headers: dict,
operations: list[str]) -> list[dict]:
ct = _CT["JSON-API"]
ep_path = f"{base_ep}/{model}"
eps = []
if "List Records" in operations:
eps.append({
"name": f"List {model}",
"method": "GET",
"path": ep_path,
"headers": {**headers, "Accept": ct},
"params": {
"page[number]": "1",
"page[size]": "10",
f"fields[{model}]": "id,name,display_name",
"sort": "id",
},
"body": "",
"body_type": "raw",
"content_type": "",
"test_script": _jsonapi_test(200),
"description": f"Fetch paginated list of {model} records with field selection, sorting and filtering.",
})
if "Get Single Record" in operations:
eps.append({
"name": f"Get {model} by ID",
"method": "GET",
"path": f"{ep_path}/{{{{id}}}}",
"headers": {**headers, "Accept": ct},
"params": {f"fields[{model}]": "id,name,display_name"},
"body": "",
"body_type": "raw",
"content_type": "",
"test_script": _jsonapi_test(200),
"description": f"Fetch a single {model} record by its database ID.",
})
if "Create Record" in operations:
body = json.dumps({
"data": {
"type": model,
"attributes": {"name": f"New {model.split('.')[-1].replace('_', ' ').title()}"},
}
}, indent=2)
eps.append({
"name": f"Create {model}",
"method": "POST",
"path": ep_path,
"headers": {**headers, "Content-Type": ct, "Accept": ct},
"params": {},
"body": body,
"body_type": "raw",
"content_type": ct,
"test_script": _jsonapi_test(201),
"description": f"Create a new {model} record.",
})
if "Update Record" in operations:
body = json.dumps({
"data": {
"type": model,
"id": "{{id}}",
"attributes": {"name": "Updated Name"},
}
}, indent=2)
eps.append({
"name": f"Update {model}",
"method": "PATCH",
"path": f"{ep_path}/{{{{id}}}}",
"headers": {**headers, "Content-Type": ct, "Accept": ct},
"params": {},
"body": body,
"body_type": "raw",
"content_type": ct,
"test_script": _jsonapi_test(200),
"description": f"Update an existing {model} record by ID.",
})
if "Delete Record" in operations:
eps.append({
"name": f"Delete {model}",
"method": "DELETE",
"path": f"{ep_path}/{{{{id}}}}",
"headers": {**headers, "Accept": ct},
"params": {},
"body": "",
"body_type": "raw",
"content_type": "",
"test_script": _jsonapi_test(200),
"description": f"Delete a {model} record by ID.",
})
if "Execute Method" in operations:
body = json.dumps({
"data": {
"type": model,
"id": "{{id}}",
"attributes": {
"method": "action_confirm",
"args": [],
"kwargs": {},
},
}
}, indent=2)
eps.append({
"name": f"Execute Method on {model}",
"method": "POST",
"path": f"{ep_path}/{{{{id}}}}/execute",
"headers": {**headers, "Content-Type": ct, "Accept": ct},
"params": {},
"body": body,
"body_type": "raw",
"content_type": ct,
"test_script": _rest_test(200),
"description": f"Execute an ORM method on a {model} record. Change 'action_confirm' to any valid method name.",
})
if "Export to Excel" in operations:
body = json.dumps({
"data": {
"type": model,
"attributes": {
"fields": ["id", "name", "display_name"],
"ids": [],
},
}
}, indent=2)
eps.append({
"name": f"Export {model} to Excel",
"method": "POST",
"path": f"{base_ep}/export",
"headers": {**headers, "Content-Type": ct, "Accept": ct},
"params": {},
"body": body,
"body_type": "raw",
"content_type": ct,
"test_script": _rest_test(200),
"description": "Returns a Base64-encoded Excel file with the specified fields.",
})
if "Generate Report" in operations:
body = json.dumps({
"data": {
"type": model,
"attributes": {
"report": f"{model.split('.')[0]}.report_{model.split('.')[-1]}",
"ids": ["{{id}}"],
"format": "pdf",
},
}
}, indent=2)
eps.append({
"name": f"Generate Report for {model}",
"method": "POST",
"path": f"{base_ep}/report",
"headers": {**headers, "Content-Type": ct, "Accept": ct},
"params": {},
"body": body,
"body_type": "raw",
"content_type": ct,
"test_script": _rest_test(200),
"description": "Generate a PDF/HTML/TEXT report. Change format to 'html' or 'text' as needed.",
})
if "Get Fields" in operations:
eps.append({
"name": f"Get Fields — {model}",
"method": "GET",
"path": f"{ep_path}/fields_get",
"headers": {**headers, "Accept": ct},
"params": {"attributes": "string,type,required,help"},
"body": "",
"body_type": "raw",
"content_type": "",
"test_script": _rest_test(200),
"description": f"Get all field definitions for {model}.",
})
if "Check Access Rights" in operations:
eps.append({
"name": f"Check Access — {model}",
"method": "GET",
"path": f"{ep_path}/check_access_rights",
"headers": {**headers, "Accept": ct},
"params": {"operation": "read"},
"body": "",
"body_type": "raw",
"content_type": "",
"test_script": _rest_test(200),
"description": "Check if the current user has the specified access right (read/write/create/unlink).",
})
return eps
def _build_restjson_endpoints(base_ep: str, model: str, headers: dict,
operations: list[str]) -> list[dict]:
ct = _CT["REST JSON"]
ep_path = f"{base_ep}/{model}"
eps = []
if "List Records" in operations:
eps.append({
"name": f"List {model}",
"method": "GET",
"path": ep_path,
"headers": {**headers, "Accept": ct},
"params": {"page": "1", "limit": "10", "fields": "id,name,display_name", "sort": "id"},
"body": "",
"body_type": "raw",
"content_type": "",
"test_script": _rest_test(200),
"description": f"Fetch paginated list of {model} records.",
})
if "Get Single Record" in operations:
eps.append({
"name": f"Get {model} by ID",
"method": "GET",
"path": f"{ep_path}/{{{{id}}}}",
"headers": {**headers, "Accept": ct},
"params": {"fields": "id,name,display_name"},
"body": "",
"body_type": "raw",
"content_type": "",
"test_script": _rest_test(200),
"description": f"Fetch a single {model} record by ID.",
})
if "Create Record" in operations:
body = json.dumps({"name": f"New {model.split('.')[-1].title()}"}, indent=2)
eps.append({
"name": f"Create {model}",
"method": "POST",
"path": ep_path,
"headers": {**headers, "Content-Type": ct},
"params": {},
"body": body,
"body_type": "raw",
"content_type": ct,
"test_script": _rest_test(201),
"description": f"Create a new {model} record.",
})
if "Update Record" in operations:
body = json.dumps({"name": "Updated Name"}, indent=2)
eps.append({
"name": f"Update {model}",
"method": "PATCH",
"path": f"{ep_path}/{{{{id}}}}",
"headers": {**headers, "Content-Type": ct},
"params": {},
"body": body,
"body_type": "raw",
"content_type": ct,
"test_script": _rest_test(200),
"description": f"Update an existing {model} record.",
})
if "Delete Record" in operations:
eps.append({
"name": f"Delete {model}",
"method": "DELETE",
"path": f"{ep_path}/{{{{id}}}}",
"headers": {**headers},
"params": {},
"body": "",
"body_type": "raw",
"content_type": "",
"test_script": _rest_test(200),
"description": f"Delete a {model} record by ID.",
})
if "Execute Method" in operations:
body = json.dumps({"method": "action_confirm", "args": [], "kwargs": {}}, indent=2)
eps.append({
"name": f"Execute Method on {model}",
"method": "POST",
"path": f"{ep_path}/{{{{id}}}}/execute",
"headers": {**headers, "Content-Type": ct},
"params": {},
"body": body,
"body_type": "raw",
"content_type": ct,
"test_script": _rest_test(200),
"description": "Execute an ORM method on a record.",
})
if "Get Fields" in operations:
eps.append({
"name": f"Get Fields — {model}",
"method": "GET",
"path": f"{ep_path}/fields_get",
"headers": {**headers},
"params": {},
"body": "",
"body_type": "raw",
"content_type": "",
"test_script": _rest_test(200),
"description": f"Get field definitions for {model}.",
})
return eps
def _build_graphql_endpoints(base_ep: str, model: str, headers: dict,
operations: list[str]) -> list[dict]:
ct = _CT["GraphQL"]
gql = _model_gql(model)
path = f"{base_ep}/graphql"
eps = []
if "List Records" in operations:
query = (
f"query {{\n"
f" {gql}(\n"
f" filter: \"\"\n"
f" pageSize: 10\n"
f" pageNumber: 1\n"
f" ) {{\n"
f" id\n"
f" name\n"
f" display_name\n"
f" }}\n"
f"}}"
)
eps.append({
"name": f"GraphQL — List {model}",
"method": "POST",
"path": path,
"headers": {**headers, "Content-Type": ct},
"params": {},
"body": json.dumps({"query": query}, indent=2),
"body_type": "raw",
"content_type": ct,
"test_script": _rest_test(200),
"description": f"GraphQL query to list {model} records.",
})
if "Get Single Record" in operations:
query = (
f"query {{\n"
f" {gql}(id: {{{{id}}}}) {{\n"
f" id\n"
f" name\n"
f" display_name\n"
f" }}\n"
f"}}"
)
eps.append({
"name": f"GraphQL — Get {model} by ID",
"method": "POST",
"path": path,
"headers": {**headers, "Content-Type": ct},
"params": {},
"body": json.dumps({"query": query}, indent=2),
"body_type": "raw",
"content_type": ct,
"test_script": _rest_test(200),
"description": f"GraphQL query to get a single {model} record.",
})
if "Create Record" in operations:
mutation = (
f"mutation {{\n"
f" create_{gql}(\n"
f" attributes: {{\n"
f" name: \"New Record\"\n"
f" }}\n"
f" ) {{\n"
f" id\n"
f" name\n"
f" }}\n"
f"}}"
)
eps.append({
"name": f"GraphQL — Create {model}",
"method": "POST",
"path": path,
"headers": {**headers, "Content-Type": ct},
"params": {},
"body": json.dumps({"query": mutation}, indent=2),
"body_type": "raw",
"content_type": ct,
"test_script": _rest_test(200),
"description": f"GraphQL mutation to create a {model} record.",
})
if "Update Record" in operations:
mutation = (
f"mutation {{\n"
f" update_{gql}(\n"
f" id: {{{{id}}}}\n"
f" attributes: {{\n"
f" name: \"Updated Name\"\n"
f" }}\n"
f" ) {{\n"
f" id\n"
f" name\n"
f" }}\n"
f"}}"
)
eps.append({
"name": f"GraphQL — Update {model}",
"method": "POST",
"path": path,
"headers": {**headers, "Content-Type": ct},
"params": {},
"body": json.dumps({"query": mutation}, indent=2),
"body_type": "raw",
"content_type": ct,
"test_script": _rest_test(200),
"description": f"GraphQL mutation to update a {model} record.",
})
if "Delete Record" in operations:
mutation = (
f"mutation {{\n"
f" delete_{gql}(id: {{{{id}}}}) {{\n"
f" id\n"
f" }}\n"
f"}}"
)
eps.append({
"name": f"GraphQL — Delete {model}",
"method": "POST",
"path": path,
"headers": {**headers, "Content-Type": ct},
"params": {},
"body": json.dumps({"query": mutation}, indent=2),
"body_type": "raw",
"content_type": ct,
"test_script": _rest_test(200),
"description": f"GraphQL mutation to delete a {model} record.",
})
return eps
# ── Main entry point ──────────────────────────────────────────────────────────
def generate_collection(
instance_url: str,
endpoint: str,
api_kind: str,
auth_type: str,
auth_creds: dict,
models: list[str],
operations: list[str],
collection_name: str = "",
) -> dict:
"""
Generate an EKIKA API Framework collection dict ready for storage.
Returns same structure as ai_client.analyze_docs().
"""
base_url = instance_url.rstrip("/")
base_ep = _clean_endpoint(endpoint)
headers = _auth_headers(auth_type)
env_vars = _env_vars(instance_url, auth_type, auth_creds)
all_endpoints = []
for model in models:
model = model.strip()
if not model:
continue
if api_kind == "JSON-API":
all_endpoints += _build_jsonapi_endpoints(base_ep, model, headers, operations)
elif api_kind == "REST JSON":
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
all_endpoints += _build_restjson_endpoints(base_ep, model, headers, operations)
# Build URLs using {{base_url}} variable
for ep in all_endpoints:
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])}"
return {
"collection_name": name,
"base_url": base_url,
"auth_type": auth_type.lower().replace(" ", "_"),
"doc_type": "ekika_odoo_framework",
"endpoints": all_endpoints,
"environment_variables": env_vars,
"notes": (
f"API Kind: {api_kind} | Auth: {auth_type}\n"
f"Endpoint: {base_url}{base_ep}/{{model}}\n"
f"Replace {{{{id}}}} with actual record IDs before sending."
),
"_source": "ekika_odoo",
}