Metadata-Version: 2.5
Name: relay-metis
Version: 0.1.0
Summary: Lightweight Python client for Metis, Relay's Cube-backed metrics layer.
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: pandas>=2.0
Requires-Dist: requests>=2.31
Description-Content-Type: text/markdown

# metis

Lightweight Python client for Metis, Relay's Cube-backed metrics layer. Queries a Metis
view and returns a typed, annotated pandas DataFrame — the metrics you see in dashboards,
in a notebook, computed the same way.

## Setup

```bash
export METIS_API_URL="https://<deployment>.cubecloudapp.dev"   # /cubejs-api/v1 optional
export METIS_API_KEY="<pre-minted Cube API token>"
```

Both can also be passed explicitly: `MetisClient(url=..., api_key=...)` (constructor
arguments win over the environment).

## Usage

```python
from datetime import date
from metis import MetisClient

client = MetisClient()

df = client.query(
    "orders",
    measures=["order_count", "revenue_sum"],
    dimensions=["status"],
    time_dimension="created_at",
    granularity="day",
    date_range=(date(2026, 8, 1), date(2026, 8, 31)),
    filters={"is_priority": True},
    order={"created_at": "asc"},
)
```

Member names are unprefixed and scoped to the view; result columns come back
prefix-stripped. Discovery:

```python
client.meta()  # one row per view/member: kind, type, title, description
```

Unknown views/members fail before the query is sent, with did-you-mean suggestions.

### Filters

The mapping form is sugar for equals/in (`None` means "is not set"):

```python
filters = {"status": ["shipped", "delivered"], "is_priority": True}
```

Anything richer takes raw Cube filter dicts (member names still unprefixed):

```python
filters = [{"member": "revenue_sum", "operator": "gt", "values": ["1000"]}]
```

### Dtypes

Model-driven, never inferred from whichever values a result happens to contain:

| Member | dtype |
| --- | --- |
| count / count_distinct measures | `Int64` (nullable) |
| all other numeric measures | `float64` |
| numeric dimensions, integer-typed at source | `Int64` (nullable) |
| numeric dimensions, decimal-typed at source | `float64` |
| booleans | `boolean` (nullable) |
| time members | `datetime64` |

Numeric dimensions are classified from the raw serialisation: integer columns always
arrive as bare digit strings and classify correctly. Known caveat: a decimal-typed
dimension (FLOAT64 or NUMERIC) whose result values are all whole also arrives as bare
digits and reads as `Int64` until a decimal value appears. Override any column
per-call: `dtypes={"weight_grams": "float64"}`.

Query provenance (payload, Cube annotation, request time) is attached under
`df.attrs["metis"]`.

### Escape hatch

```python
client.load(
    {"query": {...}, "cache": "must-revalidate"}
)  # raw Cube REST payload -> raw dict
```

## Behaviour notes

- Cache mode is hard-coded to `must-revalidate`: serves cached data while current,
  never stale data.
- Long-running queries are polled (Cube "Continue wait") within a wall-clock budget of
  120s by default — raise `total_wait_budget_seconds` for heavy queries.
- Transient errors retry up to 3 times with backoff; auth failures and query errors
  fail immediately and loudly.
