← back to Rentv Sheet Enrich Refine
lib.py
101 lines
#!/usr/bin/env python3
"""
lib.py — pure-stdlib Google Sheets read/write helpers with light-green fill marking.
No external deps (no gspread / google-api-python-client). Refreshes an access token
from SHEETS_REFRESH_TOKEN + the desktop OAuth client, then talks to the Sheets REST API.
Every cell this library writes is stamped with a LIGHT-GREEN background so Steve can
see at a glance what was auto-filled vs. what was already in the sheet.
"""
import os, re, json, urllib.parse, urllib.request
SID = os.environ.get("SHEET_ID", "1yDmtj9Wxe71SaoQm8xpFsn722B4Ws67hTtPIHeXoN9E")
ENV_PATH = os.path.expanduser("~/Projects/secrets-manager/.env")
API = "https://sheets.googleapis.com/v4/spreadsheets"
# Light green fill for auto-populated cells (≈ #cbe6b4)
GREEN = {"red": 0.796, "green": 0.902, "blue": 0.706}
def _env():
d = {}
try:
for ln in open(ENV_PATH):
m = re.match(r'^([A-Z0-9_]+)=(.*)$', ln.rstrip("\n"))
if m: d[m.group(1)] = m.group(2)
except FileNotFoundError:
pass
return d
def access_token():
e = _env()
cid = e.get("YT_UPLOAD_CLIENT_ID"); csec = e.get("YT_UPLOAD_CLIENT_SECRET")
rt = e.get("SHEETS_REFRESH_TOKEN")
if not rt:
raise SystemExit("Not connected yet — run connect.py first (SHEETS_REFRESH_TOKEN missing).")
data = urllib.parse.urlencode({
"client_id": cid, "client_secret": csec,
"refresh_token": rt, "grant_type": "refresh_token"}).encode()
tok = json.load(urllib.request.urlopen(
urllib.request.Request("https://oauth2.googleapis.com/token", data)))
return tok["access_token"]
def _req(method, url, tok, body=None):
data = json.dumps(body).encode() if body is not None else None
r = urllib.request.Request(url, data=data, method=method,
headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"})
try:
return json.load(urllib.request.urlopen(r))
except urllib.error.HTTPError as ex:
raise SystemExit(f"Sheets API {method} {url.split('?')[0]} -> {ex.code}: {ex.read().decode()[:500]}")
def get_meta(tok):
return _req("GET", f"{API}/{SID}?fields=sheets(properties(sheetId,title,gridProperties))", tok)
def read_tab(tok, title):
"""Return the tab's values as a list of rows (list of str), padded ragged."""
# A1 notation needs the sheet title wrapped in single quotes when it has spaces/slashes
rng = urllib.parse.quote("'" + title.replace("'", "''") + "'", safe="")
res = _req("GET", f"{API}/{SID}/values/{rng}?majorDimension=ROWS", tok)
return res.get("values", [])
def col_letter(idx0):
s = ""; n = idx0
while True:
s = chr(ord('A') + n % 26) + s; n = n // 26 - 1
if n < 0: break
return s
def batch_fill(tok, sheet_id, cells):
"""
cells: list of dicts {row0, col0, value} (0-based row/col indices)
Writes each value AND stamps a light-green background, in ONE batchUpdate.
"""
def mk(c):
uev = {"formulaValue": str(c["value"])} if c.get("formula") else {"stringValue": str(c["value"])}
return {"updateCells": {
"start": {"sheetId": sheet_id, "rowIndex": c["row0"], "columnIndex": c["col0"]},
"rows": [{"values": [{
"userEnteredValue": uev,
"userEnteredFormat": {"backgroundColor": GREEN}}]}],
"fields": "userEnteredValue,userEnteredFormat.backgroundColor"}}
if not cells:
return {"skipped": True}
return _chunked(tok, sheet_id, [mk(c) for c in cells])
def clear_cells(tok, sheet_id, coords):
"""coords: list of (row0,col0) — blank the value AND reset background to white."""
reqs = [{"updateCells": {
"start": {"sheetId": sheet_id, "rowIndex": r, "columnIndex": c},
"rows": [{"values": [{"userEnteredValue": None,
"userEnteredFormat": {"backgroundColor": {"red":1,"green":1,"blue":1}}}]}],
"fields": "userEnteredValue,userEnteredFormat.backgroundColor"}} for r, c in coords]
return _chunked(tok, sheet_id, reqs)
def _chunked(tok, sheet_id, reqs):
CHUNK, total = 400, 0
for i in range(0, len(reqs), CHUNK):
_req("POST", f"{API}/{SID}:batchUpdate", tok, {"requests": reqs[i:i+CHUNK]})
total += len(reqs[i:i+CHUNK])
return {"totalUpdatedCells": total}