"""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", }