← back to Rentv Sheet Enrich Refine
Split Contact Name -> First Name + Last Name across ALL 19 name-bearing tabs (parallel, quota-safe values+green writes, auto grid-expand); handles Contact Name/Contact Name(s)/Contact variants + title-row headers
48eb172f69518dfaa174888cd7b0d2fd038ad5ba · 2026-08-13 13:33:05 -0700 · Steve Abrams
Files touched
Diff
commit 48eb172f69518dfaa174888cd7b0d2fd038ad5ba
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Aug 13 13:33:05 2026 -0700
Split Contact Name -> First Name + Last Name across ALL 19 name-bearing tabs (parallel, quota-safe values+green writes, auto grid-expand); handles Contact Name/Contact Name(s)/Contact variants + title-row headers
---
split_names.py | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 104 insertions(+)
diff --git a/split_names.py b/split_names.py
new file mode 100644
index 0000000..b359094
--- /dev/null
+++ b/split_names.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+"""
+split_names.py — add "First Name" + "Last Name" columns to EVERY tab that has a
+"Contact Name" column, parsed from that name. Quota-safe: writes each tab's two columns
+as ONE values-range update (not per-cell) + ONE green-format call = 2 API calls/tab, so
+it never trips the 60-writes/min Sheets limit. Handles "First Last", "First M. Last",
+"Last, First"; skips non-person rows. Columns appended at the right (non-destructive).
+Runs tabs with light parallelism + 429 retry. ADD-only: won't clobber existing values.
+"""
+import lib, re, time, urllib.request, urllib.error
+from concurrent.futures import ThreadPoolExecutor
+
+SUFFIX = {"jr","sr","ii","iii","iv","cpa","esq","mba","phd","mai","ccim"}
+GREEN = lib.GREEN
+NAME_LABELS = ["Contact Name", "Contact Name(s)", "Contact Person", "Full Name", "Contact", "Name", "Client"]
+
+def split(name):
+ name = (name or "").strip()
+ if not name or not name[0].isalpha(): return "", ""
+ # these tabs are "First Last"; a comma/&/;/"and" separates MULTIPLE people -> take the first
+ name = re.split(r'\s*(?:[,;&/]|\band\b)\s*', name)[0].strip()
+ toks = [t for t in name.split() if t]
+ while len(toks) > 1 and re.sub(r'[^a-z]', '', toks[-1].lower()) in SUFFIX: toks.pop()
+ if not toks: return "", ""
+ if len(toks) == 1: return toks[0], ""
+ return toks[0], toks[-1]
+
+def find_header(rows):
+ """Return (header_row_index, name_col_index) or (None, None)."""
+ for hr in range(min(5, len(rows))):
+ row = [c.strip() for c in rows[hr]]
+ for lbl in NAME_LABELS:
+ if lbl in row:
+ return hr, row.index(lbl)
+ return None, None
+
+def req(method, url, tok, body, tries=5):
+ for a in range(tries):
+ try:
+ r = urllib.request.Request(url, data=(None if body is None else __import__("json").dumps(body).encode()),
+ method=method, headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"})
+ return __import__("json").load(urllib.request.urlopen(r))
+ except urllib.error.HTTPError as e:
+ if e.code == 429 and a < tries-1:
+ time.sleep(8 * (a+1)); continue
+ raise RuntimeError(f"{e.code}: {e.read().decode()[:300]}")
+
+def process(tok, title, gid):
+ rows = lib.read_tab(tok, title)
+ if not rows: return (title, "empty")
+ hr, NAME = find_header(rows)
+ if hr is None: return (title, "skip (no name column)")
+ hdr = [h.strip() for h in rows[hr]]
+ def g(r, i): return (r[i] if i < len(r) else "").strip()
+ right = max((i for i, v in enumerate(hdr) if v.strip()), default=0)
+ existF = hdr.index("First Name") if "First Name" in hdr else None
+ existL = hdr.index("Last Name") if "Last Name" in hdr else None
+ FN = existF if existF is not None else right + 1
+ LN = existL if existL is not None else (FN + 1)
+ # ensure the grid is wide enough (expand columns if needed) before writing
+ width = max(len(r) for r in rows)
+ need = LN + 1
+ if need > width:
+ req("POST", f"{lib.API}/{lib.SID}:batchUpdate", tok, {"requests": [{"appendDimension": {
+ "sheetId": gid, "dimension": "COLUMNS", "length": need - width}}]})
+ # build full-length columns: blanks above header, header at hr, split values below
+ fcol = [[""] for _ in range(len(rows))]; lcol = [[""] for _ in range(len(rows))]
+ fcol[hr] = ["First Name"]; lcol[hr] = ["Last Name"]
+ filled = 0
+ for ri in range(hr + 1, len(rows)):
+ first, last = split(g(rows[ri], NAME))
+ if existF is not None and g(rows[ri], existF): first = g(rows[ri], existF) # ADD-only
+ if existL is not None and g(rows[ri], existL): last = g(rows[ri], existL)
+ fcol[ri] = [first]; lcol[ri] = [last]
+ filled += (1 if first else 0) + (1 if last else 0)
+ fL, lL = lib.col_letter(FN), lib.col_letter(LN)
+ req("POST", f"{lib.API}/{lib.SID}/values:batchUpdate", tok, {
+ "valueInputOption": "RAW",
+ "data": [
+ {"range": f"'{title}'!{fL}1:{fL}{len(fcol)}", "values": fcol},
+ {"range": f"'{title}'!{lL}1:{lL}{len(lcol)}", "values": lcol},
+ ]})
+ def greenreq(col):
+ return {"repeatCell": {
+ "range": {"sheetId": gid, "startRowIndex": hr, "endRowIndex": len(rows),
+ "startColumnIndex": col, "endColumnIndex": col+1},
+ "cell": {"userEnteredFormat": {"backgroundColor": GREEN}},
+ "fields": "userEnteredFormat.backgroundColor"}}
+ req("POST", f"{lib.API}/{lib.SID}:batchUpdate", tok, {"requests": [greenreq(FN), greenreq(LN)]})
+ return (title, f"header@row{hr} '{hdr[NAME]}' -> split {len(rows)-1-hr} rows ({filled} cells)")
+
+def main():
+ tok = lib.access_token()
+ sheets = [(s["properties"]["title"], s["properties"]["sheetId"]) for s in lib.get_meta(tok)["sheets"]]
+ def safe(ts):
+ try: return process(tok, ts[0], ts[1])
+ except Exception as e: return (ts[0], f"ERROR {e}")
+ with ThreadPoolExecutor(max_workers=3) as ex: # 3 tabs at a time * 2 calls = well under 60/min
+ results = list(ex.map(safe, sheets))
+ for title, msg in results:
+ print(f" {title[:36]:36s} -> {msg}")
+
+if __name__ == "__main__":
+ main()
← 0d5bb2d auto-data-snapshot: 2026-08-13T13:27:38 (3 data files) — dat
·
back to Rentv Sheet Enrich Refine
·
Rearchitect console: serve from OUR OWN local snapshot (snap ef8ce09 →