2023-05-04 11:00:00 -07:00
|
|
|
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
2023-07-07 07:43:45 -07:00
|
|
|
import type { PyDict } from 'pyodide/ffi';
|
2023-05-04 11:00:00 -07:00
|
|
|
import { LoadPyodide } from './Pyodide';
|
|
|
|
import type { SandboxContext } from './Sandbox';
|
|
|
|
import { Sandbox } from './Sandbox';
|
|
|
|
|
|
|
|
type PythonSandboxContext = {
|
|
|
|
[K in keyof SandboxContext as K extends `$${infer I}` ? `_${I}` : K]: SandboxContext[K];
|
|
|
|
};
|
|
|
|
|
|
|
|
type PyodideError = Error & { type: string };
|
|
|
|
|
|
|
|
const envAccessBlocked = process.env.N8N_BLOCK_ENV_ACCESS_IN_NODE === 'true';
|
|
|
|
|
|
|
|
export class PythonSandbox extends Sandbox {
|
|
|
|
private readonly context: PythonSandboxContext;
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
context: SandboxContext,
|
|
|
|
private pythonCode: string,
|
|
|
|
itemIndex: number | undefined,
|
|
|
|
helpers: IExecuteFunctions['helpers'],
|
|
|
|
) {
|
|
|
|
super(
|
|
|
|
{
|
|
|
|
object: {
|
|
|
|
singular: 'dictionary',
|
|
|
|
plural: 'dictionaries',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
itemIndex,
|
|
|
|
helpers,
|
|
|
|
);
|
|
|
|
// Since python doesn't allow variable names starting with `$`,
|
|
|
|
// rename them to all to start with `_` instead
|
|
|
|
this.context = Object.keys(context).reduce((acc, key) => {
|
|
|
|
acc[key.startsWith('$') ? key.replace(/^\$/, '_') : key] = context[key];
|
|
|
|
return acc;
|
|
|
|
}, {} as PythonSandboxContext);
|
|
|
|
}
|
|
|
|
|
|
|
|
async runCodeAllItems() {
|
|
|
|
const executionResult = await this.runCodeInPython<INodeExecutionData[]>();
|
|
|
|
return this.validateRunCodeAllItems(executionResult);
|
|
|
|
}
|
|
|
|
|
|
|
|
async runCodeEachItem() {
|
|
|
|
const executionResult = await this.runCodeInPython<INodeExecutionData>();
|
|
|
|
return this.validateRunCodeEachItem(executionResult);
|
|
|
|
}
|
|
|
|
|
|
|
|
private async runCodeInPython<T>() {
|
2023-07-07 07:43:45 -07:00
|
|
|
const packageCacheDir = this.helpers.getStoragePath();
|
|
|
|
const pyodide = await LoadPyodide(packageCacheDir);
|
2023-05-04 11:00:00 -07:00
|
|
|
|
|
|
|
let executionResult;
|
|
|
|
try {
|
2023-07-07 07:43:45 -07:00
|
|
|
await pyodide.runPythonAsync('jsproxy_typedict[0] = type(Object.new().as_object_map())');
|
|
|
|
|
|
|
|
await pyodide.loadPackagesFromImports(this.pythonCode);
|
|
|
|
|
2023-05-04 11:00:00 -07:00
|
|
|
const dict = pyodide.globals.get('dict');
|
2023-07-07 07:43:45 -07:00
|
|
|
const globalsDict: PyDict = dict();
|
2023-05-04 11:00:00 -07:00
|
|
|
for (const key of Object.keys(this.context)) {
|
|
|
|
if ((key === '_env' && envAccessBlocked) || key === '_node') continue;
|
|
|
|
const value = this.context[key];
|
|
|
|
globalsDict.set(key, value);
|
|
|
|
}
|
|
|
|
|
2023-08-01 08:47:43 -07:00
|
|
|
pyodide.setStdout({ batched: (str) => this.emit('output', str) });
|
2023-07-07 07:43:45 -07:00
|
|
|
|
|
|
|
const runCode = `
|
|
|
|
async def __main():
|
|
|
|
${this.pythonCode
|
|
|
|
.split('\n')
|
|
|
|
.map((line) => ' ' + line)
|
|
|
|
.join('\n')}
|
|
|
|
await __main()`;
|
2023-05-04 11:00:00 -07:00
|
|
|
executionResult = await pyodide.runPythonAsync(runCode, { globals: globalsDict });
|
|
|
|
globalsDict.destroy();
|
|
|
|
} catch (error) {
|
|
|
|
throw this.getPrettyError(error as PyodideError);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (executionResult?.toJs) {
|
|
|
|
return executionResult.toJs({
|
|
|
|
dict_converter: Object.fromEntries,
|
|
|
|
create_proxies: false,
|
|
|
|
}) as T;
|
|
|
|
}
|
|
|
|
|
|
|
|
return executionResult as T;
|
|
|
|
}
|
|
|
|
|
|
|
|
private getPrettyError(error: PyodideError): Error {
|
|
|
|
const errorTypeIndex = error.message.indexOf(error.type);
|
|
|
|
if (errorTypeIndex !== -1) {
|
|
|
|
return new Error(error.message.slice(errorTypeIndex));
|
|
|
|
}
|
|
|
|
|
|
|
|
return error;
|
|
|
|
}
|
|
|
|
}
|