UNPKG

n8n-nodes-python-raw

Version:

🚨 WARNING: 100% AI-GENERATED EXPERIMENTAL CODE - HIGH RISK! Use at own risk, not for production. Python execution for n8n with file processing, multiple credentials, debug tools. Personal use only - Commons Clause license.

1,046 lines (1,027 loc) • 144 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateCodeTemplateStatic = exports.PythonFunction = void 0; const n8n_workflow_1 = require("n8n-workflow"); const child_process_1 = require("child_process"); const fs = require("fs"); const path = require("path"); const tempy = require("tempy"); function getNodeVersionFromPackage() { try { let packageJson; try { packageJson = require('../../../package.json'); } catch (e1) { try { packageJson = require('../../package.json'); } catch (e2) { console.warn('Could not load package.json dynamically, using fallback version'); return 14; } } const version = packageJson.version; const versionParts = version.split('.'); return parseInt(versionParts[1], 10) || 1; } catch (error) { console.warn('Failed to read package version, using fallback version 14'); return 14; } } async function cleanupScript(scriptPath) { try { if (fs.existsSync(scriptPath)) { fs.unlinkSync(scriptPath); console.log(`Cleaned up script: ${scriptPath}`); } } catch (error) { console.warn(`Failed to cleanup script ${scriptPath}:`, error); } } function detectBinaryFiles(items) { const binaryFiles = []; for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { const item = items[itemIndex]; if (item.binary) { for (const [key, binaryData] of Object.entries(item.binary)) { if (binaryData && binaryData.data && binaryData.fileName) { binaryFiles.push({ key, data: { data: binaryData.data, fileName: binaryData.fileName, mimeType: binaryData.mimeType || 'application/octet-stream', fileExtension: binaryData.fileExtension, }, itemIndex, }); } } } } return binaryFiles; } function validateFile(binaryFile, options) { const buffer = Buffer.from(binaryFile.data.data, 'base64'); const sizeInMB = buffer.length / (1024 * 1024); if (sizeInMB > options.maxFileSize) { throw new Error(`File "${binaryFile.data.fileName}" is too large: ${sizeInMB.toFixed(2)}MB > ${options.maxFileSize}MB`); } console.log(`File "${binaryFile.data.fileName}" validated: ${sizeInMB.toFixed(2)}MB, type: ${binaryFile.data.mimeType}`); } function getFileExtension(filename) { const extension = path.extname(filename).toLowerCase(); return extension.startsWith('.') ? extension.substring(1) : extension; } function sanitizeFileName(filename) { return filename.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_').substring(0, 255); } async function createTemporaryFiles(binaryFiles, options) { const fileMappings = []; for (const binaryFile of binaryFiles) { try { validateFile(binaryFile, options); const buffer = Buffer.from(binaryFile.data.data, 'base64'); const extension = getFileExtension(binaryFile.data.fileName); const sanitizedFileName = sanitizeFileName(binaryFile.data.fileName); const fileMapping = { filename: binaryFile.data.fileName, mimetype: binaryFile.data.mimeType, size: buffer.length, binaryKey: binaryFile.key, itemIndex: binaryFile.itemIndex, extension, }; if (options.accessMethod === 'base64' || options.accessMethod === 'both') { fileMapping.base64Data = binaryFile.data.data; } if (options.accessMethod === 'temp_files' || options.accessMethod === 'both') { const tempPath = tempy.file({ extension: extension || 'bin', }); await fs.promises.writeFile(tempPath, binaryFile.data.data, 'base64'); fileMapping.tempPath = tempPath; console.log(`Created temporary file: ${tempPath} (${(buffer.length / 1024).toFixed(1)}KB)`); } fileMappings.push(fileMapping); } catch (error) { console.error(`Failed to process file "${binaryFile.data.fileName}":`, error); throw new Error(`File processing failed for "${binaryFile.data.fileName}": ${error.message}`); } } return fileMappings; } async function cleanupTemporaryFiles(fileMappings) { let cleanedCount = 0; let errorCount = 0; for (const fileMapping of fileMappings) { if (fileMapping.tempPath) { try { if (fs.existsSync(fileMapping.tempPath)) { await fs.promises.unlink(fileMapping.tempPath); cleanedCount++; } } catch (error) { errorCount++; console.warn(`Failed to cleanup temporary file: ${fileMapping.tempPath}`, error); } } } if (cleanedCount > 0) { console.log(`Cleaned up ${cleanedCount} temporary files`); } if (errorCount > 0) { console.warn(`Failed to cleanup ${errorCount} temporary files`); } } class PythonFunction { constructor() { this.description = { displayName: 'Python Function (Raw)', name: 'pythonFunctionRaw', icon: 'fa:code', group: ['transform'], version: getNodeVersionFromPackage(), description: 'Run custom Python script once and return raw output (exitCode, stdout, stderr)', defaults: { name: 'PythonFunctionRaw', color: '#4B8BBE', }, inputs: ['main'], outputs: ['main'], credentials: [ { name: 'pythonEnvVars', required: false, }, ], properties: [ { displayName: 'Credentials Management', name: 'credentialsManagement', type: 'collection', default: {}, placeholder: 'Add Credential Options', description: 'Configure how credentials are included in the Python script', options: [ { displayName: 'Use Default Credential', name: 'useDefaultCredential', type: 'boolean', default: true, description: 'Include the credential selected in "Credential to connect with" dropdown above', }, { displayName: 'Multiple Credentials Method', name: 'multipleCredentialsMethod', type: 'options', options: [ { name: 'None (Use Default Only)', value: 'none', description: 'Use only the default credential from "Credential to connect with"', }, { name: 'Credential Names List', value: 'names_list', description: 'Enter credential names as comma-separated text', }, { name: 'Additional Credential Selectors', value: 'selectors', description: 'Use multiple credential selector dropdowns', }, { name: 'Dynamic Credential Collection', value: 'collection', description: 'Add/remove credential selectors dynamically', }, { name: 'JSON Configuration', value: 'json_config', description: 'Define credentials using JSON configuration', }, ], default: 'none', description: 'Choose how to add multiple credentials to the script', }, { displayName: 'Credential Names', name: 'credentialNamesList', type: 'string', default: '', placeholder: 'credential1, credential2, credential3', description: 'Enter credential names separated by commas (must match exact credential names)', displayOptions: { show: { multipleCredentialsMethod: ['names_list'], }, }, }, { displayName: 'Additional Credential 1', name: 'additionalCredential1', type: 'string', default: '', placeholder: 'Enter credential name', description: 'Enter name of first additional Python Environment Variables credential', displayOptions: { show: { multipleCredentialsMethod: ['selectors'], }, }, }, { displayName: 'Additional Credential 2', name: 'additionalCredential2', type: 'string', default: '', placeholder: 'Enter credential name', description: 'Enter name of second additional Python Environment Variables credential', displayOptions: { show: { multipleCredentialsMethod: ['selectors'], }, }, }, { displayName: 'Additional Credential 3', name: 'additionalCredential3', type: 'string', default: '', placeholder: 'Enter credential name', description: 'Enter name of third additional Python Environment Variables credential', displayOptions: { show: { multipleCredentialsMethod: ['selectors'], }, }, }, { displayName: 'Credential Collection', name: 'credentialCollection', type: 'fixedCollection', default: { credentials: [] }, typeOptions: { multipleValues: true, }, description: 'Add multiple credentials dynamically', displayOptions: { show: { multipleCredentialsMethod: ['collection'], }, }, options: [ { displayName: 'Credentials', name: 'credentials', values: [ { displayName: 'Credential Name', name: 'credentialName', type: 'string', default: '', placeholder: 'Enter credential name', description: 'Enter name of Python Environment Variables credential', }, { displayName: 'Variable Prefix', name: 'variablePrefix', type: 'string', default: '', placeholder: 'API1_, PROD_, etc.', description: 'Optional prefix to add to all variables from this credential', }, ], }, ], }, { displayName: 'JSON Configuration', name: 'jsonConfiguration', type: 'json', default: '{\n "credentials": [\n {\n "name": "credential1",\n "prefix": "API1_"\n },\n {\n "name": "credential2",\n "prefix": "PROD_"\n }\n ],\n "mergeStrategy": "last_wins"\n}', description: 'Define credentials configuration in JSON format', displayOptions: { show: { multipleCredentialsMethod: ['json_config'], }, }, }, { displayName: 'Variable Merge Strategy', name: 'mergeStrategy', type: 'options', options: [ { name: 'Last Selected Wins', value: 'last_wins', description: 'If variables have same name, last credential wins', }, { name: 'First Selected Wins', value: 'first_wins', description: 'If variables have same name, first credential wins', }, { name: 'Prefix with Source', value: 'prefix_source', description: 'Add credential name prefix to variables (e.g., CRED1_API_KEY)', }, { name: 'Skip Conflicts', value: 'skip_conflicts', description: 'Skip variables that would conflict (keep first occurrence)', }, ], default: 'last_wins', description: 'How to handle variable name conflicts between credentials', displayOptions: { show: { multipleCredentialsMethod: ['names_list', 'selectors', 'collection'], }, }, }, { displayName: 'Hide Variable Values in Generated Script', name: 'hideVariableValues', type: 'boolean', default: false, description: 'Replace variable values with asterisks in generated scripts (for security)', }, ], }, { displayName: 'Python Code', name: 'functionCode', typeOptions: { alwaysOpenEditWindow: true, rows: 15, }, type: 'string', default: `# Example: Use with "Inject Variables" enabled (default) # Environment and credentials variables are available as individual variables # Control what's included via Script Generation Options import json import sys # Input data from previous nodes (if "Include input_items Array" enabled) print("Input items count:", len(input_items)) # Environment variables are available individually: # MY_API_KEY, DB_HOST, PORT, etc. (from credentials or system env) # Binary files from previous nodes (if "File Processing" enabled) if 'input_files' in globals() and input_files: print(f"Found {len(input_files)} files:") for file_info in input_files: filename = file_info['filename'] size_mb = file_info['size'] / (1024 * 1024) file_type = file_info['extension'] print(f" - {filename} ({size_mb:.2f}MB, type: {file_type})") # Access file content if 'temp_path' in file_info: # Read via temporary file path (recommended) file_path = file_info['temp_path'] with open(file_path, 'rb') as f: content = f.read() print(f" Read {len(content)} bytes from {file_path}") elif 'base64_data' in file_info: # Read via base64 data import base64 content = base64.b64decode(file_info['base64_data']) print(f" Decoded {len(content)} bytes from base64") # Legacy env_vars dict (if "Include env_vars Dictionary" enabled) # print("Environment variables in dict:", len(env_vars)) result = {"processed_count": len(input_items), "status": "success"} if 'input_files' in globals(): result["files_processed"] = len(input_files) print(json.dumps(result)) # ===== OR ===== # Disable "Inject Variables" to run pure Python code: # # import requests # pip install requests # response = requests.get("https://api.github.com/users/octocat") # print(response.json()) `, description: 'Python script to execute. Use "Inject Variables" option to access input_items and env_vars.', noDataExpression: true, }, { displayName: 'Code Template Mode', name: 'codeTemplateMode', type: 'boolean', default: false, description: 'Enable code template extraction to view auto-generated Python code structure', }, { displayName: 'Extract Code Template', name: 'extractCodeTemplate', type: 'options', noDataExpression: true, options: [], default: '', typeOptions: { loadOptionsMethod: 'generateCodeTemplate', }, displayOptions: { show: { codeTemplateMode: [true], }, }, description: '🔄 Click the refresh button to generate code template based on current node settings', }, { displayName: 'Auto-Generated Code Template', name: 'autoGeneratedCode', type: 'string', displayOptions: { show: { codeTemplateMode: [true], }, }, typeOptions: { alwaysOpenEditWindow: false, rows: 20, editor: 'code', editorLanguage: 'python', }, default: `# ===== AUTO-GENERATED PYTHON CODE TEMPLATE ===== # This template shows the code structure that n8n automatically generates # around your Python code. # # 🔄 IMPORTANT: Click the refresh button above to generate the current template # based on your node settings (Script Generation Options, File Processing, etc.) # # The template will show: # • Environment variables from credentials (as individual Python variables) # • Input data from previous nodes (if "Include input_items Array" is enabled) # • Binary file processing variables (if "File Processing" is enabled) # • Output directory for generated files (if "Output File Processing" is enabled) # • All necessary imports and boilerplate code # Example template structure: #!/usr/bin/env python3 # Auto-generated script for n8n Python Function (Raw) import json import sys # Environment variables (from credentials and system) API_KEY = "your_api_key_here" # From: main_credential DATABASE_URL = "postgresql://user:pass@host:5432/db" # From: main_credential DEBUG = "true" # From: main_credential # Input data from previous nodes input_items = [ {"id": 1, "name": "Sample Item 1", "status": "active"}, {"id": 2, "name": "Sample Item 2", "status": "pending"} ] # Binary files from previous nodes (if File Processing enabled) input_files = [ { "filename": "example.txt", "mimetype": "text/plain", "size": 1024, "extension": "txt", "temp_path": "/tmp/example.txt", "base64_data": "ZXhhbXBsZSBjb250ZW50" } ] # Output directory for generated files (if Output File Processing enabled) output_dir = r"/tmp/n8n_output_YYYYMMDD_HHMMSS_random" expected_filename = "result.json" output_file_path = r"/tmp/n8n_output_YYYYMMDD_HHMMSS_random/result.json" # === YOUR PYTHON CODE WOULD GO HERE === # 🔄 Click "Extract Code Template" button above to see the ACTUAL template # generated for your current node configuration!`, description: 'Shows the auto-generated Python code template with current node settings. Click the refresh button above to update with your current configuration.', placeholder: 'Click "Extract Code Template" button above to generate current template...', }, { displayName: 'Inject Variables', name: 'injectVariables', type: 'boolean', default: false, description: 'Whether to inject input_items and env_vars variables. Enable for n8n integration features. Disable for pure Python scripts (recommended).', }, { displayName: 'Python Executable', name: 'pythonPath', type: 'string', default: 'python3', description: 'Path to Python executable (python3, python, or full path)', }, { displayName: 'Error Handling', name: 'errorHandling', type: 'options', options: [ { name: 'Return Error Details', value: 'details', description: 'Continue execution and return error information as output data (default behavior)', }, { name: 'Throw Error on Non-Zero Exit', value: 'throw', description: 'Stop workflow execution if script exits with non-zero code or system error occurs', }, { name: 'Ignore Exit Code', value: 'ignore', description: 'Continue execution regardless of exit code, only throw on system errors', }, ], default: 'details', description: 'How to handle Python script errors and non-zero exit codes', }, { displayName: 'Debug/Test Mode', name: 'debugMode', type: 'options', options: [ { name: 'Off', value: 'off', description: 'Normal execution without debug information (default)', }, { name: 'Basic Debug', value: 'basic', description: 'Add script content and basic execution info to output', }, { name: 'Full Debug', value: 'full', description: 'Add script content, metadata, timing, and detailed execution info', }, { name: 'Test Only', value: 'test', description: 'Validate script and show preview without executing (safe testing)', }, { name: 'Export Script', value: 'export', description: 'Full debug information plus script file as binary attachment', }, ], default: 'off', description: 'Choose debug and testing options for script development and troubleshooting', }, { displayName: 'Script Export Format', name: 'scriptExportFormat', type: 'options', displayOptions: { show: { debugMode: ['export'], }, }, options: [ { name: 'Python File (.py)', value: 'py', description: 'Export as .py file (standard Python script format)', }, { name: 'Text File (.txt)', value: 'txt', description: 'Export as .txt file (useful when .py files are blocked by security policies)', }, ], default: 'py', description: 'Choose the file format for script export in debug mode', }, { displayName: 'Script Generation Options', name: 'scriptOptions', type: 'collection', default: {}, placeholder: 'Add Option', options: [ { displayName: 'Include input_items Array', name: 'includeInputItems', type: 'boolean', default: true, description: 'Include input_items array in script (for accessing input data from previous nodes)', }, { displayName: 'Include env_vars Dictionary', name: 'includeEnvVarsDict', type: 'boolean', default: false, description: 'Include env_vars dictionary in script (for legacy compatibility - variables are already available individually)', }, { displayName: 'System Environment Variables', name: 'systemEnvVars', type: 'multiOptions', default: [], description: 'Select system environment variables to include in the script', options: [], typeOptions: { loadOptionsMethod: 'getSystemEnvVars', }, }, ], }, { displayName: 'File Processing', name: 'fileProcessing', type: 'collection', default: {}, placeholder: 'Add File Options', description: 'Configure processing of binary files from previous nodes', options: [ { displayName: 'Enable File Processing', name: 'enabled', type: 'boolean', default: false, description: 'Automatically extract binary files from input and make them available in Python script', }, { displayName: 'File Access Method', name: 'accessMethod', type: 'options', options: [ { name: 'Temporary Files (Recommended)', value: 'temp_files', description: 'Save files to temporary paths accessible in script', }, { name: 'Base64 Content', value: 'base64', description: 'Provide file content as base64 strings', }, { name: 'Both Methods', value: 'both', description: 'Provide both temporary file paths and base64 content', }, ], default: 'temp_files', description: 'How to provide file access in the Python script', displayOptions: { show: { enabled: [true], }, }, }, { displayName: 'Max File Size (MB)', name: 'maxFileSize', type: 'number', typeOptions: { minValue: 1, maxValue: 1000, }, default: 100, description: 'Maximum file size to process (1-1000 MB)', displayOptions: { show: { enabled: [true], }, }, }, { displayName: 'Include File Metadata', name: 'includeMetadata', type: 'boolean', default: true, description: 'Include file metadata (size, mimetype, etc.) in script variables', displayOptions: { show: { enabled: [true], }, }, }, { displayName: 'Auto-cleanup Temporary Files', name: 'autoCleanup', type: 'boolean', default: true, description: 'Automatically delete temporary files after script execution', displayOptions: { show: { enabled: [true], accessMethod: ['temp_files', 'both'], }, }, }, ], }, { displayName: 'Output File Processing', name: 'outputFileProcessing', type: 'collection', default: {}, placeholder: 'Add Output File Options', description: 'Configure automatic detection and processing of files generated by Python script', options: [ { displayName: 'Enable Output File Processing', name: 'enabled', type: 'boolean', default: false, description: 'Automatically detect and process files created by Python script in output directory', }, { displayName: 'Max Output File Size (MB)', name: 'maxOutputFileSize', type: 'number', typeOptions: { minValue: 1, maxValue: 1000, }, default: 100, description: 'Maximum output file size to process (1-1000 MB)', displayOptions: { show: { enabled: [true], }, }, }, { displayName: 'Auto-cleanup Output Directory', name: 'autoCleanupOutput', type: 'boolean', default: true, description: 'Automatically delete output directory and files after processing', displayOptions: { show: { enabled: [true], }, }, }, { displayName: 'Include File Metadata in Output', name: 'includeOutputMetadata', type: 'boolean', default: true, description: 'Include file metadata (size, mimetype, etc.) in output JSON', displayOptions: { show: { enabled: [true], }, }, }, { displayName: 'Auto-Intercept File Operations', name: 'autoInterceptFiles', type: 'boolean', default: true, description: 'Automatically redirect all file write operations to output directory (works with any script without modification)', displayOptions: { show: { enabled: [true], }, }, }, { displayName: 'Expected Output Filename', name: 'expectedFileName', type: 'string', default: 'result.json', placeholder: 'report.pdf, data.csv, result.json, etc.', description: 'Filename you expect the Python script to create (required for automatic file detection)', displayOptions: { show: { enabled: [true], }, }, }, { displayName: 'File Detection Mode', name: 'fileDetectionMode', type: 'options', options: [ { name: 'Ready Variable Path', value: 'variable_path', description: 'Add output_file_path variable with full path to your script (recommended)', }, { name: 'Auto Search by Name', value: 'auto_search', description: 'Automatically find file by name after script execution', }, ], default: 'variable_path', description: 'How to provide the output file path to your Python script', displayOptions: { show: { enabled: [true], }, }, }, ], }, { displayName: 'Parse Output', name: 'parseOutput', type: 'options', options: [ { name: 'None (Raw String)', value: 'none', description: 'Return stdout as raw string', }, { name: 'JSON', value: 'json', description: 'Parse stdout as JSON object', }, { name: 'Lines', value: 'lines', description: 'Split stdout into array of lines', }, { name: 'Smart Auto-detect', value: 'smart', description: 'Automatically detect and parse JSON, CSV, or return lines', }, ], default: 'none', description: 'How to parse the stdout output for easier data access', }, { displayName: 'Parse Options', name: 'parseOptions', type: 'collection', placeholder: 'Add Option', default: {}, displayOptions: { show: { parseOutput: ['json', 'smart'], }, }, options: [ { displayName: 'Handle Multiple JSON Objects', name: 'multipleJson', type: 'boolean', default: false, description: 'Parse multiple JSON objects separated by newlines', }, { displayName: 'Strip Non-JSON Text', name: 'stripNonJson', type: 'boolean', default: true, description: 'Remove non-JSON text before and after JSON content', }, { displayName: 'Fallback on Parse Error', name: 'fallbackOnError', type: 'boolean', default: true, description: 'Keep original stdout if parsing fails', }, ], }, { displayName: 'Execution Mode', name: 'executionMode', type: 'options', options: [ { name: 'Once for All Items', value: 'once', description: 'Execute script once with all input items available (faster)', }, { name: 'Once per Item', value: 'perItem', description: 'Execute script separately for each input item (slower but more flexible)', }, ], default: 'once', description: 'Choose how many times to execute the Python script', }, { displayName: 'Pass Through Input Data', name: 'passThrough', type: 'boolean', default: false, description: 'Include original input data in the output alongside Python script results', }, { displayName: 'Pass Through Mode', name: 'passThroughMode', type: 'options', displayOptions: { show: { passThrough: [true], }, }, options: [ { name: 'Merge with Result', value: 'merge', description: 'Add input fields directly to the result object', }, { name: 'Separate Field', value: 'separate', description: 'Add input data as "inputData" field in result', }, { name: 'Multiple Outputs', value: 'multiple', description: 'Return separate items for input data and Python result', }, ], default: 'separate', description: 'How to include input data in the output', }, { displayName: 'File Debug Options', name: 'fileDebugOptions', type: 'collection', default: {}, placeholder: 'Add Debug Options', description: 'Advanced debugging options for file processing issues', options: [ { displayName: 'Enable File Debugging', name: 'enabled', type: 'boolean', default: false, description: 'Include detailed file processing information in output for troubleshooting', }, { displayName: 'Debug Input Files', name: 'includeInputFileDebug', type: 'boolean', default: true, description: 'Include detailed information about input files processing', displayOptions: { show: { enabled: [true], }, }, }, { displayName: 'Debug Output Files', name: 'includeOutputFileDebug', type: 'boolean', default: true, description: 'Include detailed information about output files and directory scanning', displayOptions: { show: { enabled: [true], }, }, }, { displayName: 'Include System Information', name: 'includeSystemInfo', type: 'boolean', default: true, description: 'Include system information like permissions, disk space, environment variables', displayOptions: { show: { enabled: [true], }, }, }, { displayName: 'Include Directory Listings', name: 'includeDirectoryListing', type: 'boolean', default: false, description: 'Include file listings from working directory, temp directory, and output directory', displayOptions: { show: { enabled: [true], }, }, }, ], }, ], }; this.methods = { loadOptions: { async generateCodeTemplate() { try { const currentNodeParameter = this.getCurrentNodeParameter; const functionCode = currentNodeParameter('functionCode') || ''; const scriptOptions = currentNodeParameter('scriptOptions') || {}; const fileProcessing = currentNodeParameter('fileProcessing') || {}; const outputFileProcessing = currentNodeParameter('outputFileProcessing') || {}; const credentialsManagement = currentNodeParameter('credentialsManagement') || {}; const templateCode = generateCodeTemplateStatic(functionCode, scriptOptions.includeInputItems !== false, scriptOptions.includeEnvVarsDict === true, credentialsManagement.hideVariableValues === true, fileProcessing.enabled === true, outputFileProcessing.enabled === true); const lines = templateCode.split('\n'); const nonEmptyLines = lines.filter(line => line.trim() !== '').length; const timestamp = new Date().toLocaleString(); const hasUserCode = functionCode && functionCode.trim(); const previewLines = lines.slice(0, 20); const preview = previewLines.join('\n') + (lines.length > 20 ? '\n\n# ... (truncated)' : ''); return [ { name: `🔄 Generated Template (${lines.length} li