Metadata-Version: 2.4
Name: minerva-sdk
Version: 0.0.14
Summary: Official Python SDK for the Minerva Data API — resolve, enrich, validate, usage.
Project-URL: Documentation, https://docs.minerva.io/sdk/introduction
Author: Minerva Data
License: MIT
License-File: LICENSE
Keywords: enrichment,identity-resolution,minerva
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx<1,>=0.27
Requires-Dist: pydantic<3,>=2.6
Provides-Extra: all
Requires-Dist: gspread<7,>=6; extra == 'all'
Requires-Dist: openpyxl>=3; extra == 'all'
Requires-Dist: pandas>=2; extra == 'all'
Requires-Dist: tabulate>=0.9; extra == 'all'
Provides-Extra: dev
Requires-Dist: gspread<7,>=6; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: openpyxl>=3; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Provides-Extra: excel
Requires-Dist: openpyxl>=3; extra == 'excel'
Provides-Extra: gsheet
Requires-Dist: gspread<7,>=6; extra == 'gsheet'
Provides-Extra: pandas
Requires-Dist: pandas>=2; extra == 'pandas'
Provides-Extra: table
Requires-Dist: tabulate>=0.9; extra == 'table'
Description-Content-Type: text/markdown

<p align="center">
  <img src="https://app.minerva.io/minerva-logo.png" alt="Minerva" width="220">
</p>

# Minerva SDK

The official Python SDK for the Minerva Data API —
identity **resolution**, person **enrichment**, and validation, with one client, typed responses,
and input validation baked in.

![Python](https://img.shields.io/badge/python-3.11%E2%80%933.14-blue)
![Types](https://img.shields.io/badge/typed-pydantic%20v2-blue)

```python
from minerva import Minerva

mc = Minerva()  # reads MINERVA_API_KEY from the environment

mc.status.health()          # quick liveness check (unauthenticated) -> {"api": HealthStatus(ok=True, ...)}

results = mc.api.enrich([{"record_id": "1", "linkedin_url": "https://www.linkedin.com/in/example"}])
df = results.to_df()        # straight to a DataFrame
```

> **Early access.** Data API surface — resolve, enrich, LinkedIn lookup, email validation,
> country inference, and usage tallies — is wired and tested.

---

## Installation

```bash
pip install minerva-sdk
```

Optional extras:

```bash
pip install "minerva-sdk[pandas]"   # results.to_df()
pip install "minerva-sdk[table]"    # results.to_table()
pip install "minerva-sdk[gsheet]"   # mc.io.*_from_sheet / .to_sheet
pip install "minerva-sdk[excel]"    # mc.io.*_from_excel / .to_excel
pip install "minerva-sdk[all]"      # everything
```

Requires **Python 3.11+** (tested through 3.14).

---

## Authentication

`Minerva` is the single entry point. The only credential you need is your **API key**:

| Variable | Used for |
|---|---|
| `MINERVA_API_KEY` | every call the SDK makes |

> Find your key in your Minerva account (not available on the free tier) — full steps in the [docs](https://docs.minerva.io/sdk/authentication).

```python
mc = Minerva()                          # api_key from MINERVA_API_KEY
mc = Minerva(api_key="mk_live_...")     # explicit
```

The SDK sends the API key as the `x-api-key` header on every request. The server-side authorizer
decides what your key is entitled to.

---

## Quickstart

```python
from minerva import Minerva

mc = Minerva(api_key="mk_live_...")

# Resolve — match records to a Minerva PID
mc.api.resolve([{"record_id": "1", "first_name": "Jane", "last_name": "Doe",
                 "emails": ["jane.doe@example.com"]}])

# Enrich — full profiles (up to 500 records per call)
resp = mc.api.enrich(
    [{"record_id": "1", "linkedin_url": "https://www.linkedin.com/in/example"}],
    match_condition_fields=["linkedin_url"],   # only count it a match if a LinkedIn URL is on file
)
for r in resp.results:
    print(r.record_id, r.is_match, r.minerva_pid, r.match_score)

# Tabular output
resp.to_df()            # pandas DataFrame   (needs [pandas])
resp.to_csv("out.csv")  # write a CSV
resp.to_dicts()         # list[dict]
```

---

## Validate before you call — `dry_run`

Every input-bearing method takes `dry_run=True`. It validates your input **locally** and returns
the exact request that *would* be sent — **without touching the network**. Invalid input raises
`MinervaValidationError` immediately, with a precise field message.

```python
from minerva import Minerva, MinervaValidationError

mc = Minerva()

req = mc.api.enrich(records, match_condition_fields=["linkedin_url"], dry_run=True)
# -> EnrichRequest (nothing sent). Inspect req.model_dump() to see the payload.

try:
    mc.api.enrich([], dry_run=True)     # 0 records — must be at least 1
except MinervaValidationError as e:
    print("caught before any API call:", e)
```

Great for checking a batch is well-formed (record limits, allowed `match_condition_fields`,
record shape, …) before spending a call.

---

## What you can do

| Namespace | Highlights |
|---|---|
| `mc.api` | `resolve`, `enrich`, `get_li_contact_info`, `validate_emails`, `infer_record_country`, `call` |
| `mc.usage` | `tallies()` — your per-endpoint API usage |
| `mc.status` | `health()` — unauthenticated liveness probe for the API |
| `mc.io` | `enrich_from_sheet` · `resolve_from_sheet` · `enrich_from_excel` · `resolve_from_excel` · `read_*` / `write_*` — run the Data API directly off Google Sheets / Excel (extras: `[gsheet]`, `[excel]`) |

Every response is a typed model with IDE autocomplete, and the list-shaped ones support
`.to_df()` / `.to_csv()` / `.to_dicts()` / `.to_table()`.

---

## Liveness & metrics — `mc.status.health()`

**Never raises** — failure is surfaced as `ok=False` with the cause, so it's safe
to call in a polling loop. The same method does two things depending on whether
you've configured an API key:

| Client state | Endpoint hit | Auth | What you get back |
|---|---|---|---|
| No API key | `GET /health` | none | `ok`, `latency_ms`, `status_code`, `status`, `message` |
| API key set | `GET /health/metrics` | `x-api-key` | Same fields **plus** `refreshed_at` (when the server last aggregated). The `message` rolls up to text like `"all systems normal"` / `"elevated latency on 2 endpoint(s)"` — no per-route numerics. |

```python
mc = Minerva()                            # MINERVA_API_KEY from env

report = mc.status.health()               # auto-picks the right endpoint

for name, h in report.items():
    if not h.ok:
        print(f"{name} {h.status}: {h.message} ({h.status_code})")
    else:
        print(f"{name} ok — {h.message} ({h.latency_ms:.0f} ms)")

# Override the auto-detect:
mc.status.health(detailed=False)          # force basic /health (skip the metrics overhead)
mc.status.health(detailed=True)           # force /health/metrics (raises MinervaAuthError if no key)

# Probe a specific endpoint:
mc.status.health(endpoints="api")
mc.status.health(endpoints=["api"])
```

### When (and when NOT) to call it

`mc.status.health()` is a **starter check**, not a per-call gate.

✅ Polling once every 30-60s from a dashboard / monitor.
✅ A one-shot call at process startup to confirm reachability.
✅ Debugging "is the API down, or is it my key?" (works without a key).

❌ Don't wrap every `mc.api.enrich(...)` / `mc.api.resolve(...)` in a health
check. Each call is still a real HTTP round-trip; gating every API call on
it doubles your latency and contributes no useful signal — the next call
itself will already raise `MinervaAPIError` if something's wrong.

The basic `/health` call (no API key) hits an API Gateway mock and returns
instantly. The authed `/health/metrics` call (API key set) goes through a
lambda backed by DataDog APM — the data is up to ~60 s stale and the call
has the usual Lambda invocation overhead. The "don't gate every API call"
rule matters more for `/health/metrics` than for `/health`, but applies to
both: a health probe is a checkpoint, not a per-request precondition.

---

## Spreadsheets — `mc.io`

Run the Data API directly off a Google Sheet or Excel workbook, and write
results back the same way. Each format is gated on an optional extra so the
base wheel stays small.

```bash
pip install "minerva-sdk[gsheet]"   # Google Sheets
pip install "minerva-sdk[excel]"    # .xlsx
```

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

# Read sheet → enrich → write results to a different tab
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",
)
```

Excel works the same way:

```python
resp = mc.io.enrich_from_excel("customers.xlsx", sheet_name="Sheet1")
resp.to_excel("enriched.xlsx")
```

**Conventions:**
- The first row of the sheet is the header. Column names map 1:1 to enrich
  fields (`record_id`, `linkedin_url`, `first_name`, `emails`, …). Use
  `field_mapping={"customer_id": "record_id"}` for renames.
- Rows above the 500-per-request limit auto-chunk; results are merged into
  one `EnrichResponse` / `ResolveResponse`.
- Google auth: pass a path to a service-account JSON, a `dict`, a built
  `google.oauth2 Credentials`, or a `gspread.Client`. Or set
  `GOOGLE_APPLICATION_CREDENTIALS` and skip the kwarg.
- Errors mirror the rest of the SDK: 401/403 → `MinervaAuthError`, 404 →
  `MinervaAPIError(status_code=404)`, malformed sheet → `MinervaValidationError`.

---

## Custom / tailored endpoints — `mc.api.call`

For client-specific routes (preview endpoints, partner integrations, paths Minerva built just
for your org) on the Data API, use the generic `mc.api.call`:

```python
result = mc.api.call("POST", "/v2/acme/lookup", json={"record_id": "abc"})
```

Same `x-api-key` auth, same error mapping, same rate-limit handling as the typed methods — the
difference is the SDK doesn't know the response schema, so you get back the raw parsed JSON.
The server enforces entitlement; callers without it see a 403 → `MinervaAuthError`.

---

## Error handling

All errors derive from `MinervaError`:

```python
from minerva import (
    MinervaValidationError,  # bad input — raised locally, before the call
    MinervaAuthError,        # 401/403 — bad key, not entitled
    MinervaRateLimitError,   # 429 — has .retry_after
    MinervaAPIError,         # other 4xx/5xx — has .status_code and .api_request_id
)

try:
    mc.api.enrich(records)
except MinervaRateLimitError as e:
    time.sleep(e.retry_after or 1)
except MinervaAPIError as e:
    print("request failed:", e.api_request_id)   # quote this to support
```

Responses are forward-compatible: new fields the API adds won't break an older client.

---

## Python support

3.11, 3.12, 3.13, 3.14. Fully type-annotated (ships `py.typed`).

## License

[MIT](LICENSE) © Minerva Data.
