> ## Documentation Index
> Fetch the complete documentation index at: https://docs.minerva.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Spreadsheets — Google Sheets & Excel

> Run resolve / enrich directly off a Google Sheet or .xlsx workbook, and write results back the same way.

`mc.io` lets you point the SDK at a spreadsheet, run the Data API on its
rows, and write the results back — without the manual "read CSV → build
records → call API → flatten response" loop.

Two formats today: **Google Sheets** (via `gspread`) and **Excel .xlsx**
(via `openpyxl`). Each is gated on its own optional extra so the base wheel
stays small.

```bash theme={null}
pip install "minerva-sdk[gsheet]"     # Google Sheets I/O
pip install "minerva-sdk[excel]"      # .xlsx I/O
```

## Google Sheets

### Read + run + write

```python theme={null}
from minerva import Minerva

mc = Minerva()  # MINERVA_API_KEY from env

# SHEET_ID is the long string between `/d/` and `/edit` in your spreadsheet's URL
SHEET_ID = "<paste-your-google-sheet-id-here>"
CREDS    = "/path/to/service-account.json"   # or a gspread.Client / dict / google Credentials

resp = mc.io.enrich_from_sheet(
    SHEET_ID,
    credentials=CREDS,
    sheet_name="Customers",
    match_condition_fields=["linkedin_url"],
)

resp.to_sheet(
    SHEET_ID,
    credentials=CREDS,
    sheet_name="Enriched",
)
```

`resolve_from_sheet` works the same way and returns a `ResolveResponse`.

### Credentials

The `credentials=` kwarg accepts whatever's most natural for your
environment — the SDK figures out what shape you passed:

| You have                                 | Pass                             |
| ---------------------------------------- | -------------------------------- |
| A service-account JSON file              | `credentials="/path/to/sa.json"` |
| Parsed service-account dict              | `credentials={...}`              |
| A built `google.oauth2 Credentials`      | `credentials=creds`              |
| A built `gspread.Client`                 | `credentials=gc`                 |
| `GOOGLE_APPLICATION_CREDENTIALS` env var | omit `credentials=` entirely     |

The env-var fallback matches Google's own conventions, so apps already
running with that variable set need no extra config.

<Tip>
  Default scopes are read-only for `enrich_from_sheet` / `resolve_from_sheet`
  / `read_sheet`, and read-write for `write_sheet` / `to_sheet`. If you pass
  a pre-built `gspread.Client` or `Credentials`, the SDK uses the scopes you
  configured — bring your own if you need something different.
</Tip>

### Field mapping

By default the **first row is the header**, and each column name maps
1:1 to an enrich field (`record_id`, `linkedin_url`, `first_name`,
`emails`, …). If your sheet has different column names, rename via
`field_mapping`:

```python theme={null}
mc.io.enrich_from_sheet(
    sheet_id,
    credentials=...,
    field_mapping={
        "customer_id": "record_id",
        "li": "linkedin_url",
    },
)
```

Columns not in the mapping pass through unchanged. Empty cells are dropped
so pydantic-Optional fields stay `None`.

### Range and tab

```python theme={null}
mc.io.enrich_from_sheet(sheet_id, credentials=..., sheet_name="Q2 Customers")
mc.io.enrich_from_sheet(sheet_id, credentials=..., range_="A1:F500")
```

Omit `sheet_name` to use the first tab. Omit `range_` to read the whole
worksheet.

### Chunking

The Data API caps `enrich` and `resolve` at 500 records per call. `mc.io`
chunks transparently — pass a 2,000-row sheet, the SDK runs four batches
and returns one merged `EnrichResponse` (the `api_request_id` field
reflects the last batch).

### Lower-level read/write

If you need just the rows (not the API call), use the raw helpers:

```python theme={null}
rows = mc.io.read_sheet(sheet_id, credentials=..., sheet_name="Tab")
# -> [["record_id", "linkedin_url"], ["1", "https://lnkd.in/a"], …]

mc.io.write_sheet(
    sheet_id,
    [["col_a", "col_b"], [1, 2], [3, 4]],
    credentials=...,
    sheet_name="Tab",
    range_="A1",          # starting cell
    mode="overwrite",     # or "append"
)
```

## Excel (.xlsx)

Same shape as Sheets, but reads from a local file path — no auth required.

```python theme={null}
resp = mc.io.enrich_from_excel(
    "customers.xlsx",
    sheet_name="Sheet1",
    match_condition_fields=["linkedin_url"],
)

resp.to_excel("enriched.xlsx")
```

If the workbook has more than one sheet, `sheet_name=` is required (we
don't pick for you):

```python theme={null}
mc.io.enrich_from_excel("workbook.xlsx", sheet_name="Q2 2026")
```

Raw row access works the same way:

```python theme={null}
rows = mc.io.read_excel("customers.xlsx")
mc.io.write_excel([["a", "b"], [1, 2]], "out.xlsx", sheet_name="Results")
```

## Writing any response to a sheet

`to_sheet()` and `to_excel()` live on every typed response — `EnrichResponse`,
`ResolveResponse`, `UsageResponse`, etc. The response is flattened the same
way `to_csv()` flattens it (union of keys across all rows, missing cells
left blank):

```python theme={null}
mc.api.enrich(records).to_sheet(sheet_id, credentials=...)
mc.usage.tallies(start="2026-01-01", end="2026-06-01").to_excel("usage.xlsx")
```

## Errors

| Condition                                            | Raises                                                   |
| ---------------------------------------------------- | -------------------------------------------------------- |
| Extra not installed (`[gsheet]` / `[excel]` missing) | `MinervaValidationError("install minerva-sdk[<extra>]")` |
| Bad credentials, 401 / 403 from Google               | `MinervaAuthError`                                       |
| Spreadsheet not found (404)                          | `MinervaAPIError(status_code=404)`                       |
| Tab not found, malformed sheet, empty rows           | `MinervaValidationError`                                 |
| Wrong file extension (Excel)                         | `MinervaValidationError`                                 |
| API errors from the underlying enrich/resolve call   | Their usual exceptions (`MinervaRateLimitError`, etc.)   |

## Setting up a Google service account (one-time)

If you don't have one yet:

1. Create a project in [Google Cloud Console](https://console.cloud.google.com/).
2. Enable the **Google Sheets API** for that project.
3. Create a service account, download its JSON key.
4. **Share the target sheet with the service account's email address**
   (it looks like `name@project.iam.gserviceaccount.com`). Without this
   step the service account can't see the sheet, even with valid creds.
5. Point `credentials=` at the JSON path, or set
   `GOOGLE_APPLICATION_CREDENTIALS=/path/to/sa.json` in your environment.

Service accounts are usually the right call for server-side / scheduled
runs. For interactive / notebook use where you'd prefer to log in as
yourself, build a `gspread.Client` with OAuth and pass it to
`credentials=`.
