# Google Sheets connector

Source: /docs/connectors/google-sheets.md

Use this as an implementation starting point: adapt the worked example to the requested dataset and the source documentation. See [Custom API](/docs/connectors/custom-api) for the shared SDK pattern and document-upload workflow.

## What this connector is for [#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](/docs/reference/python/connectors-and-syncs).

## Data and tables you can pull [#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](https://developers.google.com/workspace/sheets/api/reference/rest) 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 [#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 [#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](/docs/reference/cli/overview) for registration and configuration.

### Connection settings used by the example [#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 [#requirementsin]

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.

```text
vortexify==0.1.1
httpx>=0.27,<1
pandas>=2,<3
```

No separately installed database driver is needed by this HTTP example.

## Worked example: sync `sheet_rows` [#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` [#connection-schema-configjson]

This file contains a JSON Schema, not actual credential values. Provide those through platform configuration.

```json
{
  "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` [#connector-connectorpy]

```python
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 = None
```

### Sync schema: `syncs/sheet_rows/config.json` [#sync-schema-syncssheet_rowsconfigjson]

```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` [#sync-syncssheet_rowssyncpy]

```python
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 [#running-and-extending-the-example]

Register the connector before the sync and commit each resource separately:

```sh
vortexify create connector google-sheets
vortexify create sync google-sheets/sheet_rows
```

Edit the files, inspect `vortexify status`, and use the [CLI guide](/docs/reference/cli/overview) 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 [#sources-and-further-reading]

- [Vendor API and data reference](https://developers.google.com/workspace/sheets/api/reference/rest) — Resource definitions, authentication, fields, and source-specific limits. Check the selected resource's reference when extending the example.
- [Vortexify connector and sync contracts](/docs/reference/python/connectors-and-syncs) — Client ownership, discovery, preview isolation, and DataFrame extraction.
- [Vortexify CLI workflow](/docs/reference/cli/overview) — Project paths, resource registration, source commits, and credential configuration.
