← back to Rentv Sheet Enrich

connect.py

84 lines

#!/usr/bin/env python3
"""
connect.py — ONE-TIME Google Sheets auth for the RENTV CRE-sheet enrichment.

Reuses the existing DESKTOP-type OAuth client (YT_UPLOAD_CLIENT_ID/SECRET) that
already auto-trusts the http://localhost loopback — so there is NOTHING to register
in the Google console. Runs the consent flow once, captures a long-lived refresh
token, and stores it as SHEETS_REFRESH_TOKEN in the secure secrets store.

IMPORTANT: log in with the Google account that can EDIT the target spreadsheet.

Run it yourself in this session with the `!` prefix so the browser opens here:
  ! python3 ~/Projects/rentv-sheet-enrich/connect.py
Then click "Allow".
"""
import os, sys, json, time, urllib.parse, urllib.request, http.server, threading, webbrowser, re

ENV_PATH = os.path.expanduser("~/Projects/secrets-manager/.env")

def load_env(path):
    d = {}
    try:
        for ln in open(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 upsert_env(path, kv):
    try: lines = open(path).read().splitlines()
    except FileNotFoundError: lines = []
    keys = set(kv); out, seen = [], set()
    for ln in lines:
        m = re.match(r'^([A-Z0-9_]+)=', ln)
        if m and m.group(1) in keys:
            out.append(f'{m.group(1)}={kv[m.group(1)]}'); seen.add(m.group(1))
        else:
            out.append(ln)
    for k, v in kv.items():
        if k not in seen: out.append(f'{k}={v}')
    open(path, "w").write("\n".join(out) + "\n")

envf = load_env(ENV_PATH)
CID  = os.environ.get("YT_UPLOAD_CLIENT_ID")  or envf.get("YT_UPLOAD_CLIENT_ID")  or sys.exit("No desktop OAuth client id found (YT_UPLOAD_CLIENT_ID)")
CSEC = os.environ.get("YT_UPLOAD_CLIENT_SECRET") or envf.get("YT_UPLOAD_CLIENT_SECRET") or sys.exit("No desktop OAuth client secret found")
PORT = int(os.environ.get("SHEETS_PORT", "8733"))
REDIRECT = f"http://localhost:{PORT}/"
SCOPE = "https://www.googleapis.com/auth/spreadsheets"

hold = {}
class H(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        p = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
        if "code" in p: hold["code"] = p["code"][0]
        elif "error" in p: hold["error"] = p["error"][0]
        self.send_response(200); self.send_header("Content-Type", "text/html"); self.end_headers()
        self.wfile.write(b"<h2 style='font-family:sans-serif'>Sheets connected. You can close this tab.</h2>")
    def log_message(self, *a): pass

srv = http.server.HTTPServer(("127.0.0.1", PORT), H)
threading.Thread(target=srv.serve_forever, daemon=True).start()
auth = "https://accounts.google.com/o/oauth2/v2/auth?" + urllib.parse.urlencode({
    "client_id": CID, "redirect_uri": REDIRECT, "response_type": "code",
    "scope": SCOPE, "access_type": "offline", "prompt": "consent"})
print("AUTH_URL:", auth, flush=True)
try: webbrowser.open(auth)
except Exception: pass
print("Waiting for consent (Allow in the browser)...", flush=True)
for _ in range(300):
    if hold: break
    time.sleep(1)
srv.shutdown()
if "code" not in hold: sys.exit(f"No consent captured: {hold or 'timeout'}")

data = urllib.parse.urlencode({"code": hold["code"], "client_id": CID, "client_secret": CSEC,
    "redirect_uri": REDIRECT, "grant_type": "authorization_code"}).encode()
tok = json.load(urllib.request.urlopen(urllib.request.Request("https://oauth2.googleapis.com/token", data)))
rt = tok.get("refresh_token")
if not rt: sys.exit("No refresh_token returned (revoke prior grant + retry).")
upsert_env(ENV_PATH, {"SHEETS_REFRESH_TOKEN": rt})
print("CONNECTED — Sheets refresh token saved. Enrichment can now write to the sheet.", flush=True)