src/exporters/python.ts (141 lines of code) (raw):

import { readFileSync } from "fs"; import path from "path"; import Handlebars from "handlebars"; import { FormatExporter, ConvertOptions } from "../convert"; import { ParsedRequest } from "../parse"; import "./templates"; // this regex should match the list of APIs that do not have specific handlers // in the Python client. APIs in this list are rendered with a perform_request() // call const UNSUPPORTED_APIS = new RegExp( "^_internal.*$" + "|^connector.update_features$" + "|^connector.sync_job_.*$" + "|^ingest.get_geoip_database$" + "|^ingest.put_geoip_database$" + "|^ingest.delete_geoip_database$" + "|^security.create_cross_cluster_api_key$" + "|^security.update_cross_cluster_api_key$" + "|^security.update_settings$" + "|^security.query_user$" + "|^snapshot.repository_analyze$" + "|^watcher.get_settings$" + "|^watcher.update_settings", ); const PYCONSTANTS: Record<string, string> = { true: "True", false: "False", null: "None", }; export class PythonExporter implements FormatExporter { template: Handlebars.TemplateDelegate | undefined; // eslint-disable-next-line @typescript-eslint/no-unused-vars async check(requests: ParsedRequest[]): Promise<boolean> { // only return true if all requests are for Elasticsearch return requests .map((req) => req.service == "es") .reduce((prev, curr) => prev && curr, true); } async convert( requests: ParsedRequest[], options: ConvertOptions, ): Promise<string> { if (!(await this.check(requests))) { throw new Error("Cannot perform conversion"); } return (await this.getTemplate())({ requests, ...options }); } async getTemplate(): Promise<Handlebars.TemplateDelegate> { if (!this.template) { // custom data renderer for Python Handlebars.registerHelper("pyprint", (context) => { const lines = JSON.stringify(context ?? null, null, 4).split(/\r?\n/); for (let i = 1; i < lines.length; i++) { lines[i] = " " + lines[i]; } if (lines.length > 1) { let result = lines.join("\n"); for (const k of Object.keys(PYCONSTANTS)) { result = result.replaceAll(`${k},\n`, `${PYCONSTANTS[k]},\n`); result = result.replaceAll(`${k}\n`, `${PYCONSTANTS[k]}\n`); } return result; } else if (PYCONSTANTS[lines[0]]) { return PYCONSTANTS[lines[0]]; } else if (lines[0].startsWith('"') && lines[0].endsWith('"')) { // special case: handle strings such as "true", "false" or "null" as // their native types const s = lines[0].substring(1, lines[0].length - 1); if (PYCONSTANTS[s]) { return PYCONSTANTS[s]; } else { return lines[0]; } } else { return lines[0]; } }); // custom conditional for requests without any arguments Handlebars.registerHelper( "hasArgs", function (this: ParsedRequest, options) { if ( Object.keys(this.params ?? {}).length + Object.keys(this.query ?? {}).length + Object.keys(this.body ?? {}).length > 0 ) { return options.fn(this); } else { return options.inverse(this); } }, ); // custom conditional to separate supported vs unsupported APIs Handlebars.registerHelper( "supportedApi", function (this: ParsedRequest, options) { if (!UNSUPPORTED_APIS.test(this.api as string) && this.request) { return options.fn(this); } else { return options.inverse(this); } }, ); // attribute name renderer that considers aliases and code-specific names // arguments: // name: the name of the attribute // props: the list of schema properties this attribute belongs to Handlebars.registerHelper("alias", (name, props) => { const aliases: Record<string, string> = { from: "from_", _meta: "meta", _field_names: "field_names", _routing: "routing", _source: "source", _source_excludes: "source_excludes", _source_includes: "source_includes", }; if (aliases[name]) { return aliases[name]; } if (props) { for (const prop of props) { if (prop.name == name && prop.codegenName != undefined) { return prop.codegenName; } } } return name; }); // custom conditional to check for request body kind // the argument can be "properties" or "value" Handlebars.registerHelper( "ifRequestBodyKind", function (this: ParsedRequest, kind: string, options) { let bodyKind = this.request?.body?.kind ?? "value"; const parsedBody = typeof this.body == "object" ? this.body : {}; if (this.api == "search" && "sub_searches" in parsedBody) { // Change the kind of any search requests that use sub-searches to // "value", so that the template renders a single body argument // instead of expanding the kwargs. This is needed because the // Python client does not support "sub_searches" as a kwarg yet. bodyKind = "value"; } if (bodyKind == kind) { return options.fn(this); } else { return options.inverse(this); } }, ); if (process.env.NODE_ENV !== "test") { this.template = Handlebars.templates["python.tpl"]; } else { // when running tests we read the templates directly, in case the // compiled file is not up to date const t = readFileSync(path.join(__dirname, "./python.tpl"), "utf-8"); this.template = Handlebars.compile(t); } } return this.template; } }