This addendum documents how to connect Google Sheets to your local ExcelBridge Python server, enabling GBPY functions to work in Google Sheets exactly as they do in Excel VBA.
| Requirement | Version | Check Command |
|---|---|---|
| Node.js | 14.x or higher | node --version |
| npm | 6.x or higher | npm --version |
| ExcelBridge Server | 2.2.8+ | Running on http://127.0.0.1:8765 |
| Google Account | - | Access to Google Sheets |
Verify installation:
node --version
npm --version
Expected output: v18.x.x or higher, 9.x.x or higher
brew install node
# or download from nodejs.org
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
# Navigate to your ExcelBridge installation
cd C:\Users\YourUsername\GenePython\ExcelBridge_v2_2_8_portable
# Start the server
ExcelBridge.exe
Expected output:
ExcelBridge Server v2.2.8 running on http://127.0.0.1:8765
Press Ctrl+C to stop
cd C:\Users\YourUsername\GenePython\ExcelBridge_v2_2_8_portable
python excel_bridge_server.py
✅ KEEP THIS WINDOW OPEN AND RUNNING
LocalTunnel creates a public HTTPS URL that forwards requests to your local ExcelBridge server.
# No installation needed! npx runs it directly
npx localtunnel --port 8765 --subdomain gbpy
Expected output:
your url is: https://gbpy.loca.lt
⚠️ IMPORTANT:
gbpy1, gbpy2, excelbridge, etc.Test the tunnel:
# In another terminal window
curl https://gbpy.loca.lt/health
Expected: {"status":"ok","version":"2.2.8",...}
/**
* ============================================
* GBPY Client for Google Sheets
* ExcelBridge Python Server Connector
* Version 2.2.8
* ============================================
*
* This script connects Google Sheets to your local ExcelBridge Python server
* through a LocalTunnel HTTPS URL.
*
* AUTHOR: Gene Boo / ExcelBridge Team
* LICENSE: MIT
*
* USAGE IN GOOGLE SHEETS:
* =GBPY("plugin.py", "function", arg1, arg2, ...)
* =GBPY_PING()
* =GBPY_FUNCTIONS()
* =GBPY_ARRAY("plugin.py", "function", range)
* =GBPY_DEBUG("plugin.py", "function", args...)
*/
// ============================================
// CONFIGURATION - CHANGE THIS TO YOUR TUNNEL URL
// ============================================
/**
* !!! IMPORTANT !!!
* Replace this URL with the one from your LocalTunnel output:
* npx localtunnel --port 8765 --subdomain gbpy
*
* Example: https://gbpy.loca.lt
*/
var SERVER_BASE = "https://gbpy.loca.lt"; // ⚠️ CHANGE THIS TO YOUR URL
// API Endpoints (do not change)
var INVOKE_PATH = "/invoke";
var INVOKE_EXCEL_ARRAY_PATH = "/invoke/excel_array";
var HEALTH_PATH = "/health";
var FUNCTIONS_PATH = "/functions";
var RELOAD_PATH = "/plugins/reload";
// Cache configuration
var catalogCache = null;
var lastCatalogUpdate = 0;
var CACHE_TIMEOUT = 300000; // 5 minutes in milliseconds
// ============================================
// MAIN GBPY FUNCTION - USE THIS IN YOUR SHEETS
// ============================================
/**
* Invoke Python functions from Google Sheets
* @param {string} plugin - Plugin filename (e.g., "bs_plugin.py")
* @param {string} func - Function name (e.g., "black_scholes")
* @param {...*} args - Arguments (cell ranges, numbers, strings, dates)
* @return {*} Result - Automatically spills arrays
* @customfunction
*/
function GBPY(plugin, func, ...args) {
// Validate server URL is configured
if (SERVER_BASE === "https://gbpy.loca.lt" && SERVER_BASE.includes("localhost")) {
return "#ERROR: Please configure SERVER_BASE with your LocalTunnel URL";
}
try {
// Convert Google Sheets arguments to JSON-compatible format
var processedArgs = args.map(function(arg) {
return convertArgument(arg);
});
// Build payload (EXACT format as ExcelBridge expects)
var payload = {
plugin: String(plugin),
function: String(func),
args: processedArgs
};
// Send request to Python server
var response = httpPost(INVOKE_PATH, payload);
// Extract and format result
if (response && response.result !== undefined) {
var result = response.result;
// Handle different result types
if (typeof result === 'object' && result !== null) {
// Table with headers
if (result.data) {
return formatTableData(result);
}
// 2D array
if (Array.isArray(result)) {
return normalizeTo2D(result);
}
// Object - return as JSON string
return JSON.stringify(result);
}
// Scalar value (number, string, boolean)
return result;
}
return "#ERROR: No result in response";
} catch (e) {
return "#ERROR: " + e.toString();
}
}
// ============================================
// VARIANT FUNCTIONS - ExcelBridge API Compatibility
// ============================================
/**
* Force array output format (equivalent to PYINVOKE_ARRAY)
* @customfunction
*/
function GBPY_ARRAY(plugin, func, ...args) {
try {
var processedArgs = args.map(convertArgument);
var payload = {
plugin: plugin,
function: func,
args: processedArgs,
output_format: "excel_array"
};
var response = httpPost(INVOKE_PATH, payload);
if (response && response.result && response.result.data) {
return normalizeTo2D(response.result.data);
}
return [["#ERROR: No array data"]];
} catch (e) {
return [["#ERROR: " + e.toString()]];
}
}
/**
* Get raw JSON response (debugging)
* @customfunction
*/
function GBPY_JSON(plugin, func, ...args) {
try {
var processedArgs = args.map(convertArgument);
var payload = {
plugin: plugin,
function: func,
args: processedArgs
};
var response = httpPost(INVOKE_PATH, payload);
return JSON.stringify(response);
} catch (e) {
return "#ERROR: " + e.toString();
}
}
/**
* Ping Python server to check connectivity
* @return {string} Connection status
* @customfunction
*/
function GBPY_PING() {
try {
var response = httpGet(HEALTH_PATH);
if (response && response.status === "ok") {
return "✓ Python server connected - " + SERVER_BASE;
} else {
return "⚠ Server responded but status not OK";
}
} catch (e) {
return "✗ Connection failed: " + e.toString();
}
}
/**
* Get list of available Python functions
* @param {boolean} refresh - Force refresh cache (use TRUE to bypass cache)
* @return {Array} 2D array of function catalog
* @customfunction
*/
function GBPY_FUNCTIONS(refresh) {
try {
// Check cache
var now = new Date().getTime();
if (!refresh && catalogCache && (now - lastCatalogUpdate) < CACHE_TIMEOUT) {
return catalogCache;
}
// Fetch from server
var response = httpGet(FUNCTIONS_PATH);
// Format for Google Sheets
var output = [
["Plugin", "Function", "Description", "Signature", "Tags", "SciPy", "XGBoost"]
];
if (Array.isArray(response)) {
response.forEach(function(func) {
output.push([
func.plugin || "",
func.function || "",
func.description || "",
func.signature || "",
Array.isArray(func.tags) ? func.tags.join(", ") : "",
func.is_scipy || "",
func.is_xgboost || ""
]);
});
} else {
output.push(["No functions found", "", "", "", "", "", ""]);
}
// Update cache
catalogCache = output;
lastCatalogUpdate = now;
return output;
} catch (e) {
return [["#ERROR: " + e.toString()]];
}
}
/**
* Reload Python plugins
* @return {string} Status message
* @customfunction
*/
function GBPY_RELOAD() {
try {
var response = httpPost(RELOAD_PATH, {});
catalogCache = null; // Clear cache
return "Plugins reloaded successfully";
} catch (e) {
return "Error reloading: " + e.toString();
}
}
/**
* Debug function - inspect full server response
* @return {Array} Debug information
* @customfunction
*/
function GBPY_DEBUG(plugin, func, ...args) {
try {
var processedArgs = args.map(convertArgument);
var payload = {
plugin: plugin,
function: func,
args: processedArgs
};
var response = httpPost(INVOKE_PATH, payload);
// Format for readable display
var output = [
["=== GBPY DEBUG INFO ==="],
["Server URL:", SERVER_BASE],
["Plugin:", plugin],
["Function:", func],
["Arguments:", JSON.stringify(processedArgs)],
["Status:", response.status || "OK"],
["Result Type:", typeof response.result],
["Has Data:", response.result && response.result.data ? "Yes" : "No"],
["Full Response:"],
[JSON.stringify(response, null, 2).substring(0, 50000)]
];
return output;
} catch (e) {
return [["#ERROR: " + e.toString()]];
}
}
// ============================================
// PRIVATE HELPER FUNCTIONS - DO NOT MODIFY
// ============================================
/**
* HTTP GET request
*/
function httpGet(path) {
var url = SERVER_BASE + path;
var options = {
method: 'get',
muteHttpExceptions: true,
headers: {
'Accept': 'application/json',
'User-Agent': 'GBPY-GoogleSheets/2.2.8'
},
timeout: 30000
};
var response = UrlFetchApp.fetch(url, options);
var responseCode = response.getResponseCode();
var content = response.getContentText();
if (responseCode >= 200 && responseCode < 300) {
return JSON.parse(content);
}
throw new Error('HTTP ' + responseCode + ': ' + content.substring(0, 200));
}
/**
* HTTP POST JSON request
*/
function httpPost(path, payload) {
var url = SERVER_BASE + path;
var options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'User-Agent': 'GBPY-GoogleSheets/2.2.8'
},
timeout: 60000
};
var response = UrlFetchApp.fetch(url, options);
var responseCode = response.getResponseCode();
var content = response.getContentText();
if (responseCode >= 200 && responseCode < 300) {
return JSON.parse(content);
}
throw new Error('HTTP ' + responseCode + ': ' + content.substring(0, 200));
}
/**
* Convert Google Sheets argument to JSON-compatible format
*/
function convertArgument(arg) {
// Handle 2D arrays (cell ranges)
if (Array.isArray(arg)) {
return arg;
}
// Handle dates
if (arg instanceof Date) {
return arg.toISOString();
}
// Handle numbers, strings, booleans
if (typeof arg === 'number' || typeof arg === 'string' || typeof arg === 'boolean') {
return arg;
}
// Handle null/undefined
if (arg === null || arg === undefined) {
return null;
}
// Default: convert to string
return String(arg);
}
/**
* Normalize arrays to 2D format for Google Sheets
*/
function normalizeTo2D(arr) {
if (!Array.isArray(arr)) {
return [[arr]];
}
if (arr.length === 0) {
return [[]];
}
// Already 2D with consistent columns
if (Array.isArray(arr[0])) {
// Find maximum column count
var maxCols = 0;
arr.forEach(function(row) {
if (Array.isArray(row)) {
maxCols = Math.max(maxCols, row.length);
} else {
maxCols = Math.max(maxCols, 1);
}
});
// Pad rows to equal length
return arr.map(function(row) {
if (!Array.isArray(row)) {
row = [row];
}
var padded = row.slice();
while (padded.length < maxCols) {
padded.push("");
}
return padded;
});
}
// 1D array -> convert to column vector
return arr.map(function(item) {
return [item];
});
}
/**
* Format table data with headers
*/
function formatTableData(data) {
var result = [];
// Add header row if present
if (data.header && Array.isArray(data.header)) {
result.push(data.header);
}
// Add data rows
if (Array.isArray(data.data)) {
var normalized = normalizeTo2D(data.data);
normalized.forEach(function(row) {
result.push(row);
});
}
return result.length > 0 ? result : [["No data"]];
}
/**
* Handle large JSON responses in chunks
* @customfunction
*/
function GBPY_JSON_CHUNKS(plugin, func, ...args) {
try {
var processedArgs = args.map(convertArgument);
var payload = {
plugin: plugin,
function: func,
args: processedArgs,
output_format: "json"
};
var response = httpPost(INVOKE_PATH, payload);
var jsonStr = JSON.stringify(response);
// Google Sheets cell limit is 50,000 characters
var CHUNK_SIZE = 40000;
var chunks = [];
for (var i = 0; i < jsonStr.length; i += CHUNK_SIZE) {
chunks.push([jsonStr.substring(i, i + CHUNK_SIZE)]);
}
return chunks.length > 0 ? chunks : [["{}"]];
} catch (e) {
return [["#ERROR: " + e.toString()]];
}
}
// ============================================
// UI MENU - Runs when spreadsheet opens
// ============================================
/**
* Creates custom menu on spreadsheet open
*/
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('🐍 GBPY Python')
.addItem('Ping Server', 'showPingStatus')
.addItem('Reload Plugins', 'showReloadStatus')
.addItem('Refresh Functions', 'refreshFunctions')
.addSeparator()
.addItem('Help & About', 'showAbout')
.addToUi();
}
function showPingStatus() {
var status = GBPY_PING();
SpreadsheetApp.getUi().alert('Server Status', status, SpreadsheetApp.getUi().ButtonSet.OK);
}
function showReloadStatus() {
var status = GBPY_RELOAD();
SpreadsheetApp.getUi().alert('Reload Plugins', status, SpreadsheetApp.getUi().ButtonSet.OK);
}
function refreshFunctions() {
var sheet = SpreadsheetApp.getActiveSheet();
var result = GBPY_FUNCTIONS(true);
if (result.length > 0) {
sheet.getRange(1, 1, result.length, result[0].length).setValues(result);
SpreadsheetApp.getUi().alert('Functions refreshed', 'Catalog written to A1', SpreadsheetApp.getUi().ButtonSet.OK);
}
}
function showAbout() {
SpreadsheetApp.getUi().alert(
'GBPY for Google Sheets v2.2.8',
'Google Sheets client for ExcelBridge Python server\n\n' +
'Server URL: ' + SERVER_BASE + '\n\n' +
'Available functions:\n' +
'• =GBPY() - Main function\n' +
'• =GBPY_ARRAY() - Force array output\n' +
'• =GBPY_PING() - Test connection\n' +
'• =GBPY_FUNCTIONS() - List Python functions\n' +
'• =GBPY_DEBUG() - Inspect responses\n\n' +
'Documentation: https://excelbridge.dev',
SpreadsheetApp.getUi().ButtonSet.OK
);
}
onOpenIn your Google Sheet, type:
=GBPY_PING()
Expected result: ✓ Python server connected - https://gbpy.loca.lt
=GBPY("math_plugin.py", "add", 2, 3)
Expected result: 5
=GBPY_FUNCTIONS(TRUE)
Expected result: Table of all your Python plugins and functions
| Task | Formula |
|---|---|
| Black-Scholes | =GBPY("bs_plugin.py", "black_scholes", 100, 100, 0.2, 0.05, 0.02, 1, "call") |
| Read CSV | =GBPY("pandas_plugin.py", "read_csv", "data.csv") |
| Train Model | =GBPY_ARRAY("sklearn_plugin.py", "train", A2:B100, C2:C100) |
| Plot Chart | =GBPY("plot_plugin.py", "histogram", A2:A100, "Histogram", "Value", "Frequency") |
| Numpy Array | =GBPY_ARRAY("numpy_plugin.py", "linspace", 0, 10, 5) |
| Symptom | Cause | Solution |
|---|---|---|
✗ Connection failed |
Tunnel not running | Run npx localtunnel --port 8765 --subdomain gbpy |
#ERROR: SERVER_BASE not configured |
Wrong URL in script | Update SERVER_BASE to your LocalTunnel URL |
502 Bad Gateway |
Tunnel can’t reach server | Check ExcelBridge.exe is running on port 8765 |
Execution time exceeded |
Function > 30 seconds | Use GBPY_JSON_CHUNKS() or optimize Python code |
Subdomain already taken |
“gbpy” in use | Try gbpy1, gbpy2, excelbridge, pybridge |
┌─────────────┐ HTTPS ┌──────────────┐ HTTP ┌─────────────────┐
│ Google │ ────────────> │ LocalTunnel │ ───────────> │ ExcelBridge │
│ Sheets │ (Public URL) │ (gbpy.loca.lt) │ (Port 80) │ Python Server │
└─────────────┘ └──────────────┘ │ (localhost:8765)│
└─────────────────┘
│
┌────────▼────────┐
│ Your Plugins │
│ bs_plugin.py │
│ pandas_plugin.py│
└─────────────────┘
WINDOW 1 - EXCELBRIDGE | WINDOW 2 - TUNNEL
────────────────────────────────|────────────────────────────────
cd ExcelBridge_v2_2_8_portable | npx localtunnel --port 8765
ExcelBridge.exe | --subdomain gbpy
|
Keep running! Never close! | Keep running! Never close!
=GBPY_PING() - Test connection
=GBPY("plugin.py", "func", args) - Call any function
=GBPY_ARRAY("plugin.py", "func", range) - Force array output
=GBPY_FUNCTIONS() - List all functions
=GBPY_DEBUG("plugin.py", "func", args) - Debug mode
SERVER_BASE in Apps Script each timenpx localtunnel --port 8765 --subdomain your-company-name
Upgrade at https://localtunnel.me
// Use menu instead of cell function for >30 sec operations
function runLongCalculation() {
var result = GBPY("ml_plugin.py", "train_model", rangeValues);
// Write result to sheet
}
Create start_tunnel.bat:
@echo off
:loop
npx localtunnel --port 8765 --subdomain gbpy
echo Tunnel crashed! Restarting in 5 seconds...
timeout /t 5
goto loop
End of Google Sheets Integration Addendum
ExcelBridge v2.2.8 - Compatible with all existing Python plugins