Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Recalculate a workbook's formulas headlessly with LibreOffice.
|
||||
|
||||
openpyxl never computes formulas. This script shells out to `soffice`
|
||||
(LibreOffice) to open the workbook, recalculate, and re-save it, so
|
||||
cached formula results become available to `xlsx_read.py --data-only`
|
||||
and `--formulas`.
|
||||
|
||||
Behavior:
|
||||
* soffice on PATH: converts the file to .xlsx in a temp dir (which
|
||||
recalculates all formulas) and replaces the original (or writes
|
||||
--out). Prints {"recalculated": true, ...} and exits 0.
|
||||
* soffice absent: prints {"recalculated": false, "reason": ...} with
|
||||
installation guidance and STILL exits 0 — callers can branch on the
|
||||
JSON instead of the exit code.
|
||||
|
||||
Note: LibreOffice recalculates .xlsx on load per its default
|
||||
calculation settings; conversion re-saves with fresh cached values.
|
||||
|
||||
Usage:
|
||||
xlsx_recalc.py book.xlsx
|
||||
xlsx_recalc.py book.xlsx --out recalced.xlsx
|
||||
xlsx_recalc.py book.xlsx --timeout 120
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def count_cached(path):
|
||||
"""Number of formula cells with a cached value present."""
|
||||
from openpyxl import load_workbook
|
||||
wb_f = load_workbook(path, data_only=False)
|
||||
wb_v = load_workbook(path, data_only=True)
|
||||
formulas = cached = 0
|
||||
for name in wb_f.sheetnames:
|
||||
ws_f, ws_v = wb_f[name], wb_v[name]
|
||||
for row in ws_f.iter_rows():
|
||||
for cell in row:
|
||||
if isinstance(cell.value, str) and cell.value.startswith("="):
|
||||
formulas += 1
|
||||
if ws_v[cell.coordinate].value is not None:
|
||||
cached += 1
|
||||
return formulas, cached
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(
|
||||
description="Recalculate .xlsx formulas headlessly via LibreOffice.")
|
||||
ap.add_argument("file", help="path to .xlsx file")
|
||||
ap.add_argument("--out", help="output path (default: replace input)")
|
||||
ap.add_argument("--timeout", type=int, default=180,
|
||||
help="seconds to wait for soffice (default 180)")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
src = Path(args.file).resolve()
|
||||
if not src.exists():
|
||||
print(json.dumps({"ok": False, "error": f"no such file: {src}"}),
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
|
||||
soffice = shutil.which("soffice")
|
||||
if not soffice:
|
||||
print(json.dumps({
|
||||
"ok": True, "recalculated": False,
|
||||
"reason": "LibreOffice (soffice) not found on PATH",
|
||||
"guidance": "Install LibreOffice (e.g. `apt install "
|
||||
"libreoffice-calc` or `brew install --cask "
|
||||
"libreoffice`), or open the file in Excel/"
|
||||
"LibreOffice once and re-save it.",
|
||||
}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
proc = subprocess.run(
|
||||
[soffice, "--headless", "--calc", "--convert-to", "xlsx:Calc "
|
||||
"MS Excel 2007 XML", "--outdir", tmp, str(src)],
|
||||
capture_output=True, text=True, encoding="utf-8",
|
||||
timeout=args.timeout,
|
||||
env={"HOME": tmp, "PATH": Path(soffice).parent.as_posix()
|
||||
+ ":/usr/bin:/bin"})
|
||||
produced = Path(tmp) / (src.stem + ".xlsx")
|
||||
if proc.returncode != 0 or not produced.exists():
|
||||
print(json.dumps({"ok": False,
|
||||
"error": "soffice conversion failed",
|
||||
"stderr": proc.stderr.strip()[-500:]}),
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
formulas, cached = count_cached(produced)
|
||||
dest = Path(args.out).resolve() if args.out else src
|
||||
shutil.copyfile(produced, dest)
|
||||
|
||||
print(json.dumps({
|
||||
"ok": True, "recalculated": True, "output": str(dest),
|
||||
"formula_cells": formulas, "with_cached_values": cached,
|
||||
}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(json.dumps({"ok": False, "error": str(exc)}), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
Reference in New Issue
Block a user