spreadsheetlabeller / prepare_data.py
zofiasmolenasana
Add num_labeled_cells column to metadata sheet for per-cell billing
8811827 unverified
Raw
History Blame Contribute Delete
9.74 kB
"""Download spreadsheet data from Sheetpedia/Enron → upload to Google Drive → register in metadata.
Can be run as a CLI or called from the app's /api/ingest endpoint.
CLI usage:
python prepare_data.py --source sheetpedia --limit 10
python prepare_data.py --source enron
python prepare_data.py setup --root-folder-id 1ABC...XYZ
"""
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
import config
import drive_client
import metadata_client
STAGING_DIR = config.TEMP_DIR / "staging"
STAGING_DIR.mkdir(parents=True, exist_ok=True)
def _get_known_filenames() -> set[str]:
"""Collect filenames already uploaded to Drive / registered in metadata."""
known: set[str] = set()
try:
rows = metadata_client.get_all_rows()
for r in rows:
known.add(r["file_name"])
except Exception:
pass
for p in (config.DATA_DIR / "raw" / "sheetpedia").glob("*.xlsx"):
known.add(p.name)
return known
def ingest_sheetpedia(limit: int = 10, offset: int = 0) -> int:
"""Stream xlsx files from HuggingFace Sheetpedia, upload to Drive, register metadata.
Skips files that are already present in metadata or local raw data.
Use ``offset`` to jump ahead in the stream (handy after prior ingestions).
"""
try:
from datasets import load_dataset
except ImportError:
subprocess.check_call([sys.executable, "-m", "pip", "install", "datasets"])
from datasets import load_dataset
try:
from openpyxl import load_workbook
except ImportError:
subprocess.check_call([sys.executable, "-m", "pip", "install", "openpyxl"])
from openpyxl import load_workbook
print(f"Streaming Sheetpedia dataset (limit={limit}, offset={offset})...")
ds = load_dataset("tianzl66/Sheetpedia_xlsx", split="train", streaming=True)
known = _get_known_filenames()
print(f" Already known: {len(known)} files — will skip duplicates")
metadata_client.ensure_header()
entries = []
saved = 0
skipped = 0
for i, row in enumerate(ds):
if saved >= limit:
break
if i < offset:
continue
xlsx_bytes = row.get("xlsx")
key = row.get("__key__", f"sheet_{i}")
if xlsx_bytes is None:
continue
safe_name = Path(key).name.replace("/", "_").replace(" ", "_")
if not safe_name.endswith(".xlsx"):
safe_name += ".xlsx"
if safe_name in known:
skipped += 1
continue
if isinstance(xlsx_bytes, dict) and "bytes" in xlsx_bytes:
raw = xlsx_bytes["bytes"]
elif isinstance(xlsx_bytes, (bytes, bytearray)):
raw = bytes(xlsx_bytes)
elif isinstance(xlsx_bytes, list):
raw = bytes(xlsx_bytes)
else:
continue
local_path = STAGING_DIR / safe_name
local_path.write_bytes(raw)
try:
wb = load_workbook(str(local_path), read_only=True)
sheet_names = wb.sheetnames
wb.close()
except Exception as e:
print(f" Skipping {safe_name}: invalid xlsx ({e})")
local_path.unlink(missing_ok=True)
continue
# Also save to raw dir for local queue
raw_dir = config.DATA_DIR / "raw" / "sheetpedia"
raw_dir.mkdir(parents=True, exist_ok=True)
raw_copy = raw_dir / safe_name
if not raw_copy.exists():
raw_copy.write_bytes(local_path.read_bytes())
# Upload to Drive
drive_file_id = drive_client.upload_file(
str(local_path),
config.DRIVE_UNLABELLED_FOLDER_ID,
file_name=safe_name,
)
for sn in sheet_names:
entries.append({
"file_name": safe_name,
"drive_file_id": drive_file_id,
"sheet_name": sn,
"num_sheets_in_file": str(len(sheet_names)),
})
known.add(safe_name)
local_path.unlink(missing_ok=True)
saved += 1
if saved % 5 == 0:
print(f" Uploaded {saved}/{limit} files (skipped {skipped} duplicates)...")
added = metadata_client.add_rows(entries)
print(f"Sheetpedia: uploaded {saved} files, skipped {skipped} duplicates, registered {added} sheets")
return added
def ingest_enron() -> int:
"""Clone Enron XLS repo, convert to xlsx, upload to Drive, register metadata."""
enron_dir = STAGING_DIR / "enron"
repo_dir = enron_dir / "enron_xls"
if repo_dir.exists():
print("Enron repo already cloned, reusing...")
else:
enron_dir.mkdir(parents=True, exist_ok=True)
print("Cloning Enron XLS repo...")
subprocess.run(
["git", "clone", "--depth", "1",
"https://github.com/SheetJS/enron_xls.git",
str(repo_dir)],
check=True,
)
try:
from openpyxl import load_workbook
except ImportError:
subprocess.check_call([sys.executable, "-m", "pip", "install", "openpyxl"])
from openpyxl import load_workbook
xls_files = []
for ext in ("*.xls", "*.xlsx", "*.XLS", "*.XLSX"):
xls_files.extend(repo_dir.rglob(ext))
print(f"Found {len(xls_files)} spreadsheet files in Enron repo")
metadata_client.ensure_header()
entries = []
uploaded = 0
for xls_path in xls_files:
xlsx_path = xls_path
if xls_path.suffix.lower() == ".xls":
xlsx_path = xls_path.with_suffix(".xlsx")
if not xlsx_path.exists():
try:
import xlrd
from openpyxl import Workbook
xls_wb = xlrd.open_workbook(str(xls_path))
new_wb = Workbook()
for idx, sn in enumerate(xls_wb.sheet_names()):
xls_sheet = xls_wb.sheet_by_name(sn)
ws = new_wb.active if idx == 0 else new_wb.create_sheet(title=sn)
if idx == 0:
ws.title = sn
for r in range(xls_sheet.nrows):
for c in range(xls_sheet.ncols):
ws.cell(row=r + 1, column=c + 1, value=xls_sheet.cell_value(r, c))
new_wb.save(str(xlsx_path))
new_wb.close()
xls_wb.release_resources()
except ImportError:
print(" xlrd not installed; skipping .xls files. pip install xlrd")
break
except Exception as e:
print(f" Could not convert {xls_path.name}: {e}")
continue
try:
wb = load_workbook(str(xlsx_path), read_only=True)
sheet_names = wb.sheetnames
wb.close()
except Exception:
continue
drive_file_id = drive_client.upload_file(
str(xlsx_path),
config.DRIVE_UNLABELLED_FOLDER_ID,
file_name=xlsx_path.name,
)
for sn in sheet_names:
entries.append({
"file_name": xlsx_path.name,
"drive_file_id": drive_file_id,
"sheet_name": sn,
"num_sheets_in_file": str(len(sheet_names)),
})
uploaded += 1
if uploaded % 10 == 0:
print(f" Uploaded {uploaded} files...")
added = metadata_client.add_rows(entries)
print(f"Enron: uploaded {uploaded} files, registered {added} sheets")
return added
def setup_folders(root_folder_id: str) -> None:
"""Create Drive folder structure and print env vars to set."""
folder_ids = drive_client.setup_folder_structure(root_folder_id)
metadata_client.ensure_header()
print("\nDrive folder structure created!")
print("\nSet these environment variables:\n")
for key, value in folder_ids.items():
print(f" {key}={value}")
print(f" METADATA_SHEET_ID={config.METADATA_SHEET_ID}")
print()
def main():
parser = argparse.ArgumentParser(description="Prepare spreadsheet data for labeling")
sub = parser.add_subparsers(dest="command")
ingest_parser = sub.add_parser("ingest", help="Download and upload data")
ingest_parser.add_argument("--source", choices=["sheetpedia", "enron", "both"], default="sheetpedia")
ingest_parser.add_argument("--limit", type=int, default=10, help="Max new Sheetpedia files to download")
ingest_parser.add_argument("--offset", type=int, default=0, help="Skip first N entries in the stream")
setup_parser = sub.add_parser("setup", help="Create Drive folder structure")
setup_parser.add_argument("--root-folder-id", required=True, help="ID of the shared Drive folder")
sync_parser = sub.add_parser("sync", help="Sync metadata with Drive folder contents")
args = parser.parse_args()
if args.command == "setup":
setup_folders(args.root_folder_id)
elif args.command == "sync":
from app import sync_metadata
result = sync_metadata()
print(f"Scanned: {result['scanned']}, New sheets added: {result['new_sheets_added']}")
elif args.command == "ingest" or args.command is None:
if not hasattr(args, "source"):
args.source = "sheetpedia"
args.limit = 10
args.offset = 0
if args.source in ("sheetpedia", "both"):
ingest_sheetpedia(limit=args.limit, offset=args.offset)
if args.source in ("enron", "both"):
ingest_enron()
print("Done!")
else:
parser.print_help()
if __name__ == "__main__":
main()