File size: 1,853 Bytes
2c88096 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | """
sheets.py — pull a Google Sheet as XLSX so the existing importer can parse it.
We export the whole workbook (`export?format=xlsx`) and reuse
importer.milestones_from_xlsx, which already finds the "Brief" tab. This needs
the sheet to be shared as "Anyone with the link → Viewer" (no auth/keys).
"""
import re
import httpx
_ID_RE = re.compile(r"/spreadsheets/d/([a-zA-Z0-9-_]+)")
def extract_sheet_id(url_or_id: str) -> str:
"""Accept a full Google Sheets URL or a bare id; return the id."""
s = (url_or_id or "").strip()
if not s:
raise ValueError("empty sheet URL")
m = _ID_RE.search(s)
if m:
return m.group(1)
# assume it's already an id if it has no slashes/spaces
if "/" not in s and " " not in s:
return s
raise ValueError("could not find a spreadsheet id in that URL")
def export_url(sheet_id: str) -> str:
return f"https://docs.google.com/spreadsheets/d/{sheet_id}/export?format=xlsx"
async def fetch_xlsx(url_or_id: str) -> bytes:
"""Download the workbook as XLSX bytes. Raises RuntimeError on access issues."""
sheet_id = extract_sheet_id(url_or_id)
url = export_url(sheet_id)
async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client:
resp = await client.get(url)
if resp.status_code == 200 and "spreadsheetml" in resp.headers.get("content-type", ""):
return resp.content
# Google returns an HTML login/permission page (often 200 or 401) when the
# sheet isn't link-viewable.
if resp.status_code in (401, 403) or "text/html" in resp.headers.get("content-type", ""):
raise RuntimeError(
"Could not read the sheet. Make sure it's shared as "
"'Anyone with the link → Viewer'."
)
raise RuntimeError(f"Google Sheets export failed (HTTP {resp.status_code}).")
|