Google Sheets connector
Spreadsheet platform for tabular data, worksheet updates, and reporting.
Use this as an implementation starting point: adapt the worked example to the requested dataset and the source documentation. See Custom API for the shared SDK pattern and document-upload workflow.
What this connector is for
Spreadsheet platform for tabular data, worksheet updates, and reporting.
A connector owns access to the source. Separate syncs shape selected resources into tables for analysis and downstream transformations using the Python SDK.
Data and tables you can pull
These are representative dataset families to consider, not a claim that the worked example implements every resource. Access, fields, and availability depend on the source account, product edition, and permissions. Use the vendor reference to select concrete resources.
| Suggested table | One row represents |
|---|---|
spreadsheets | a spreadsheet |
worksheets | a worksheet |
range_rows | a worksheet-range row |
named_ranges | a named range |
Relationships and extraction considerations
A spreadsheet contains worksheets and ranges; a row's meaning comes from the sheet, not a universal Google schema. Decide how to handle headers, blank cells, and changing column layouts.
Connecting to Google Sheets
Uses a Google OAuth token with spreadsheets.readonly and access to the spreadsheet. Discovery lists grid sheet names. The sync reads columns A through last_column (default Z); cells are unformatted values. It does not infer headers or flatten user-defined schemas.
Additional resources may require additional scopes. Connection settings belong to the connector; dataset filters and resource selection belong to each sync. See the CLI workflow for registration and configuration.
Connection settings used by the example
| Setting | Requirement | Purpose |
|---|---|---|
access_token | Required; secret | Platform-supplied access token. Refresh or rotate before expiry. |
spreadsheet_id | Required | Spreadsheet Id |
requirements.in
Save these dependencies in requirements.in alongside connector.py; the example includes packages used by its sync. Install the published vortexify package for local development. Keep sync-specific dependencies in the sync's own requirements.in when splitting the implementation into separate resources.
vortexify==0.1.1
httpx>=0.27,<1
pandas>=2,<3No separately installed database driver is needed by this HTTP example.
Worked example: sync sheet_rows
sheet_rows — Reads a named grid sheet in A1 range windows, preserving row numbers and JSON cell arrays.
The complete files below implement only this example resource. They illustrate the current SDK signatures, a separate preview, bounded DataFrame output, and source-specific extraction. They do not establish complete source coverage. Live credentials and platform ingestion have not been used to validate this guide.
With default paths, place the connector files in connectors/google-sheets/ and sync files in connectors/google-sheets/syncs/sheet_rows/. Use your project configuration if it specifies different paths.
Connection schema: config.json
This file contains a JSON Schema, not actual credential values. Provide those through platform configuration.
{
"type": "object",
"properties": {
"access_token": {
"type": "string",
"title": "Access Token",
"description": "Platform-supplied access token. Refresh or rotate before expiry.",
"minLength": 1,
"x-vortexify-secret": true,
"order": 0
},
"spreadsheet_id": {
"type": "string",
"minLength": 1,
"title": "Spreadsheet Id",
"order": 1
}
},
"required": [
"access_token",
"spreadsheet_id"
]
}Connector: connector.py
import httpx
from vortexify import Column, Connector, DiscoveredTable
class GoogleSheetsConnector(Connector):
vendor_domain = "google.com"
def client(self):
if self._client is None or self._client.is_closed:
self._client = httpx.Client(
base_url="https://sheets.googleapis.com/v4/",
headers={"Authorization": "Bearer " + self.config["access_token"]},
timeout=60,
)
return self._client
def check(self) -> bool:
response = self.client().get(
"spreadsheets/" + self.config["spreadsheet_id"],
params={"fields": "spreadsheetId"},
)
response.raise_for_status()
return True
def discover(self) -> list[DiscoveredTable]:
response = self.client().get(
"spreadsheets/" + self.config["spreadsheet_id"],
params={"fields": "sheets(properties(title,sheetType,gridProperties))"},
)
response.raise_for_status()
return [
DiscoveredTable(
name=entry["properties"]["title"],
schema=[
Column(name="row_number", type="integer", nullable=False),
Column(name="cells", type="json", nullable=False),
],
)
for entry in response.json()["sheets"]
if entry["properties"].get("sheetType", "GRID") == "GRID"
]
def close(self) -> None:
if self._client is not None:
try:
self._client.close()
finally:
self._client = NoneSync schema: syncs/sheet_rows/config.json
{
"type": "object",
"properties": {
"sheet_name": {
"type": "string",
"minLength": 1,
"title": "Sheet Name",
"order": 0
},
"last_column": {
"type": "string",
"minLength": 1,
"default": "Z",
"title": "Last Column",
"order": 1
}
},
"required": [
"sheet_name"
]
}Sync: syncs/sheet_rows/sync.py
from collections.abc import Iterator
from math import isfinite
import json
import pandas as pd
from vortexify import Sync
class GoogleSheetsSheetRowsSync(Sync):
def preview(self) -> pd.DataFrame:
cfg = self.connector.config
sheet = self.config["sheet_name"]
last_column = self.config.get("last_column", "Z")
if (
not last_column.isalpha()
or not last_column.isascii()
or (not last_column.isupper())
):
raise ValueError("last_column must be uppercase column letters")
quoted_sheet = "'" + sheet.replace("'", "''") + "'"
base_path = "spreadsheets/" + quote(cfg["spreadsheet_id"], safe="")
a1 = quoted_sheet + "!A1:" + last_column + "100"
response = self.client.get(
base_path + "/values/" + quote(a1, safe=""),
params={"valueRenderOption": "UNFORMATTED_VALUE"},
)
response.raise_for_status()
rows = [
{"row_number": index + 1, "cells": json.dumps(cells, ensure_ascii=False)}
for index, cells in enumerate(response.json().get("values", []))
if cells
]
return pd.DataFrame(rows, columns=["row_number", "cells"], dtype=object)
def extract(
self, *, avg_row_size_bytes: float | None = None
) -> Iterator[pd.DataFrame]:
memory_mib = self.compute.memory if self.compute else None
memory_mib = memory_mib if memory_mib is not None and memory_mib > 0 else 1024
target_bytes = min(16 * 1024 * 1024, max(1, memory_mib * 1024 * 1024 // 20))
batch_size = 100
if avg_row_size_bytes is not None:
if not isfinite(avg_row_size_bytes) or avg_row_size_bytes <= 0:
raise ValueError("avg_row_size_bytes must be finite and positive")
batch_size = max(1, int(target_bytes / max(1.0, avg_row_size_bytes) / 4))
batch_size = min(batch_size, 1000)
cfg = self.connector.config
sheet = self.config["sheet_name"]
last_column = self.config.get("last_column", "Z")
if (
not last_column.isalpha()
or not last_column.isascii()
or (not last_column.isupper())
):
raise ValueError("last_column must be uppercase column letters")
quoted_sheet = "'" + sheet.replace("'", "''") + "'"
base_path = "spreadsheets/" + quote(cfg["spreadsheet_id"], safe="")
metadata = self.client.get(
base_path,
params={"fields": "sheets(properties(title,gridProperties(rowCount)))"},
)
metadata.raise_for_status()
properties = [
entry["properties"]
for entry in metadata.json()["sheets"]
if entry["properties"]["title"] == sheet
]
if len(properties) != 1 or "gridProperties" not in properties[0]:
raise ValueError("Named grid sheet was not found")
row_count = properties[0]["gridProperties"]["rowCount"]
for start in range(1, row_count + 1, batch_size):
end = min(row_count, start + batch_size - 1)
a1 = quoted_sheet + "!A" + str(start) + ":" + last_column + str(end)
response = self.client.get(
base_path + "/values/" + quote(a1, safe=""),
params={"valueRenderOption": "UNFORMATTED_VALUE"},
)
response.raise_for_status()
rows = [
{
"row_number": start + index,
"cells": json.dumps(cells, ensure_ascii=False),
}
for index, cells in enumerate(response.json().get("values", []))
if cells
]
if rows:
yield pd.DataFrame(rows, columns=["row_number", "cells"], dtype=object)Running and extending the example
Register the connector before the sync and commit each resource separately:
vortexify create connector google-sheets
vortexify create sync google-sheets/sheet_rowsEdit the files, inspect vortexify status, and use the CLI guide to commit and configure them. Registration alone does not run extraction or deploy the connector.
Preview must run with a fresh connector and disposable state. The extraction implementation determines full/incremental behavior; merely setting a mode does not implement change capture. API/driver failures propagate unless explicitly handled in the code. Check source limits and single-record/response sizes as well as output batch size.
Sources and further reading
- Vendor API and data reference — Resource definitions, authentication, fields, and source-specific limits. Check the selected resource's reference when extending the example.
- Vortexify connector and sync contracts — Client ownership, discovery, preview isolation, and DataFrame extraction.
- Vortexify CLI workflow — Project paths, resource registration, source commits, and credential configuration.