Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
fzumstein committed Nov 11, 2024
1 parent 4ec43ae commit 960d62a
Show file tree
Hide file tree
Showing 3 changed files with 348 additions and 11 deletions.
259 changes: 257 additions & 2 deletions app/static/js/core/custom-functions-code.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,261 @@
async function hello(name) {
let r = await window.hello(name);
// async function hello(name) {
// let r = await window.hello(name);
// return r.toJs();
// }

// CustomFunctions.associate("HELLO", hello);

// async function sql(query, tables) {
// let r = await window.sql(query, tables);
// return r.toJs();
// }

// CustomFunctions.associate("SQL", sql);

/////////////////////////////////////////

const debug = false;
let invocations = new Set();
let bodies = new Set();
let runtime;
let contentLanguage;
let socket = null;

Office.onReady(function (info) {
// Socket.io
socket = globalThis.socket ? globalThis.socket : null;

if (socket !== null) {
socket.on("disconnect", () => {
if (debug) {
console.log("disconnect");
}
for (let invocation of invocations) {
invocation.setResult([["Stream disconnected"]]);
}
invocations.clear();
});

socket.on("connect", () => {
// Without this, you'd have to hit Ctrl+Alt+F9, which isn't available on the web
if (debug) {
console.log("connect");
}
for (let body of bodies) {
socket.emit("xlwings:function-call", body);
}
});
}

// Runtime version
if (
Office.context.requirements.isSetSupported("CustomFunctionsRuntime", "1.4")
) {
runtime = "1.4";
} else if (
Office.context.requirements.isSetSupported("CustomFunctionsRuntime", "1.3")
) {
runtime = "1.3";
} else if (
Office.context.requirements.isSetSupported("CustomFunctionsRuntime", "1.2")
) {
runtime = "1.2";
} else {
runtime = "1.1";
}

// Content Language
contentLanguage = Office.context.contentLanguage;
});

function flattenVarargsArray(arr) {
// Turn [ [[0]], [ [[0]], [[0]] ] ] into:
// result: [ [[0]], [[0]], [[0]] ]
// indices: [ [ 0 ], [ 1, 0 ], [ 1, 1 ] ]
const result = [];
const indices = [];

function isTripleNested(item) {
return (
Array.isArray(item) && Array.isArray(item[0]) && Array.isArray(item[0][0])
);
}

for (let i = 0; i < arr.length; i++) {
const item = arr[i];

if (isTripleNested(item)) {
result.push(...item);
for (let j = 0; j < item.length; j++) {
indices.push([i, j]);
}
} else {
result.push(item);
indices.push([i]);
}
}

return {
result,
indices,
};
}

// Workbook name
let cachedWorkbookName = null;

async function getWorkbookName() {
if (cachedWorkbookName) {
return cachedWorkbookName;
}
const context = new Excel.RequestContext();
const workbook = context.workbook;
workbook.load("name");
await context.sync();
cachedWorkbookName = workbook.name;
return cachedWorkbookName;
}

class Semaphore {
constructor(maxConcurrency) {
this.maxConcurrency = maxConcurrency;
this.currentConcurrency = 0;
this.queue = [];
}

async acquire() {
if (this.currentConcurrency < this.maxConcurrency) {
this.currentConcurrency++;
return;
}
return new Promise((resolve) => this.queue.push(resolve));
}

release() {
this.currentConcurrency--;
if (this.queue.length > 0) {
this.currentConcurrency++;
const nextResolve = this.queue.shift();
nextResolve();
}
}
}

const semaphore = new Semaphore(1000);

async function base() {
await Office.onReady(); // Block execution until office.js is ready
// Arguments
let argsArr = Array.prototype.slice.call(arguments);
let funcName = argsArr[0];
let isStreaming = argsArr[1];
let args = argsArr.slice(2, -1);
let invocation = argsArr[argsArr.length - 1];

const workbookName = await getWorkbookName();
const officeApiClient = localStorage.getItem("Office API client");

// For arguments that are Entities, replace the arg with their address (cache key).
// The issues is that invocation.parameterAddresses returns a flat list while args
// contains a nested array for varargs (in Office.js called 'repeating').
const { result: flatArgs, indices } = flattenVarargsArray(args);

// Process each flattened item with respect to its path
flatArgs.forEach((item, index) => {
if (item && item[0][0]?.type === "Entity") {
const address = `${officeApiClient}[${workbookName}]${invocation.parameterAddresses[index]}`;

let target = args;
const path = indices[index];

for (let i = 0; i < path.length - 1; i++) {
target = target[path[i]];
}

const lastIndex = path[path.length - 1];
target[lastIndex] = [address];
}
});

// Body
let body = {
func_name: funcName,
args: args,
caller_address: `${officeApiClient}[${workbookName}]${invocation.address}`, // not available for streaming functions
content_language: contentLanguage,
version: "0.33.3",
runtime: runtime,
};

// Streaming functions communicate via socket.io
if (isStreaming) {
if (socket === null) {
console.error(
"To enable streaming functions, you need to load the socket.io js client before xlwings.min.js and custom-functions-code",
);
return;
}
let taskKey = `${funcName}_${args}`;
body.task_key = taskKey;
socket.emit("xlwings:function-call", body);
if (debug) {
console.log(`emit xlwings:function-call ${funcName}`);
}
invocation.setResult([["Waiting for stream..."]]);

socket.off(`xlwings:set-result-${taskKey}`);
socket.on(`xlwings:set-result-${taskKey}`, (data) => {
invocation.setResult(data.result);
if (debug) {
console.log(`Set Result`);
}
});

invocations.add(invocation);
bodies.add(body);

return;
}

// Normal functions communicate via REST API
return await makeJsCall(body);
}

async function makeJsCall(body) {
const processedArgs = body.args
? body.args.map((arg) => JSON.stringify(arg))
: [];

// Call function with processed arguments
let r = await window[body.func_name](...processedArgs);
return r.toJs();
}

function showError(errorMessage) {
if (
Office.context.requirements.isSetSupported("CustomFunctionsRuntime", "1.2")
) {
// Error message is only visible by hovering over the error flag!
let excelError = new CustomFunctions.Error(
CustomFunctions.ErrorCode.invalidValue,
errorMessage,
);
throw excelError;
} else {
return [[errorMessage]];
}
}

async function hello() {
let args = ["hello", false];
args.push.apply(args, arguments);
return await base.apply(null, args);
}
CustomFunctions.associate("HELLO", hello);

async function sql() {
let args = ["sql", false];
args.push.apply(args, arguments);
return await base.apply(null, args);
}
CustomFunctions.associate("SQL", sql);
98 changes: 90 additions & 8 deletions app/static/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@

import json
import os
import sqlite3

import matplotlib as mpl
import matplotlib.pyplot as plt

mpl.use("agg")
# To use matplotlib, add it to pyscript.json
# import matplotlib as mpl
# import matplotlib.pyplot as plt
# mpl.use("agg")

os.environ["XLWINGS_LICENSE_KEY"] = "noncommercial"
import xlwings as xw # noqa: E402
Expand All @@ -31,10 +32,10 @@ async def test(event):
print(sheet1["A1:A2"].value)
book.sheets[0]["A3"].value = "xxxxxxx"

fig = plt.figure()
plt.plot([1, 2, 3])
sheet1.pictures.add(fig, name="MyPlot", update=True, anchor=sheet1["C5"])
sheet1["A1"].select()
# fig = plt.figure()
# plt.plot([1, 2, 3])
# sheet1.pictures.add(fig, name="MyPlot", update=True, anchor=sheet1["C5"])
# sheet1["A1"].select()

# Process actions (this could be improved so methods are applied immediately)
xwjs.runActionsStandalone(json.dumps(book.json()))
Expand All @@ -46,3 +47,84 @@ async def hello(name):


window.hello = hello


# @func
# @arg("tables", expand="table", ndim=2)
async def sql(query, *tables):
print("xx", query)
query = json.loads(query)[0][0]
print("yy", tables)
processed_tables = [
json.loads(table) if isinstance(table, str) else table
for table in tables # TODO: remove ending [0] when converter applied
]
print("zz", processed_tables)
res = _sql(query, *processed_tables)
print("rr", res)
return res


window.sql = sql


def conv_value(value, col_is_str):
if value is None:
return "NULL"
if col_is_str:
return repr(str(value))
elif isinstance(value, bool):
return 1 if value else 0
else:
return repr(value)


def _sql(query, *tables):
conn = sqlite3.connect(":memory:")
print("t", tables)

c = conn.cursor()
for i, table in enumerate(tables):
# TODO: remove [0] in next 2 rows
cols = table[0][0]
rows = table[0][1:]
print(cols)
print(rows)
types = [any(isinstance(row[j], str) for row in rows) for j in range(len(cols))]
name = chr(65 + i)

stmt = "CREATE TABLE %s (%s)" % (
name,
", ".join(
"'%s' %s" % (col, "STRING" if typ else "REAL")
for col, typ in zip(cols, types)
),
)
c.execute(stmt)

if rows:
stmt = "INSERT INTO %s VALUES %s" % (
name,
", ".join(
"(%s)"
% ", ".join(
conv_value(value, type) for value, typ in zip(row, types)
)
for row in rows
),
)
# Fixes values like these:
# sql('SELECT a FROM a', [['a', 'b'], ["""X"Y'Z""", 'd']])
stmt = stmt.replace("\\'", "''")
c.execute(stmt)

res = []
c.execute(query)
res.append([x[0] for x in c.description])
for row in c:
res.append(list(row))
print("marker3")
print(res)
print(type(res))

return res
2 changes: 1 addition & 1 deletion app/static/pyscript.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"packages": ["xlwings", "matplotlib"]
"packages": ["xlwings", "sqlite3"]
}

0 comments on commit 960d62a

Please sign in to comment.