Metadata-Version: 2.4
Name: vibedasher
Version: 2.2.0
Summary: Official Python SDK for the Vibedasher API.
Project-URL: Homepage, https://vibedasher.com
Project-URL: Source, https://github.com/JulienGdnr/vibedasher
Author: Vibedasher
License: MIT
Keywords: analytics,api,bi,dashboards,sdk,vibedasher
Requires-Python: >=3.9
Requires-Dist: attrs>=22.2.0
Requires-Dist: httpx<0.29.0,>=0.23.0
Requires-Dist: msgpack>=1.0.0
Requires-Dist: python-dateutil>=2.8.0
Description-Content-Type: text/markdown

# vibedasher — Python SDK

Official Python SDK for the [Vibedasher](https://vibedasher.com) headless data
engine. The method you'll use most is `client.query(...)` — it runs SQL across your
**managed datasets** and returns **typed rows**, so you can render a dashboard
natively from your own backend.

## Install

```bash
pip install vibedasher
```

## Headless / eject — start here

Vibedasher hosts the data engine (ETL, datasets, query); **you own the frontend/app**.
You author a dashboard in Vibedasher, eject its code, and wire it to the engine with
a single call:

```python
from vibedasher import Vibedasher

# Server-side (API key held on your server, never shipped to a browser).
client = Vibedasher(api_key="sk_...", region="eu-central-1")

result = client.query(
    sql="SELECT region, AVG(lead_price_usd) AS avg_price FROM sales GROUP BY region",
    params={"region": "Caribe"},     # bound/escaped server-side, never interpolated
)

for col in result.columns:
    print(col.name, col.type)        # e.g. region string / avg_price number
for row in result.rows:
    print(row["region"], row["avg_price"])
```

- **`dataset_ids` is optional (plural).** Omit it and the server infers the dataset
  set from the aliases your `sql` references. Pass it to pin an exact set explicitly:
  `client.query(sql=..., dataset_ids=[12, 47], params=...)` — each id resolves
  server-side to that dataset's alias under RLS, and a dataset not in the set is
  rejected (deny-don't-drop). A mistyped/unknown alias raises
  `400 query_dataset_unresolved`.
- **`sql` is inline and alias-only** — it references dataset aliases, never a
  physical table.
- **Results are typed columnar** — `result.columns` are `QueryColumn(name, type)`
  with `type` ∈ `string | number | boolean | date | timestamp | json`, and
  `result.rows` are dicts keyed by column name.
- **Transport is hidden** — inline vs presigned object storage, MessagePack
  decoding, and transient retries all happen inside `query()`. One call in, typed
  rows out.

> `query()` is the one hand-written method (`_query.py`); everything else is
> generated (see below).

### Where the query runs: `type="wasm"`

By default the server executes your SQL and returns rows. Pass `type="wasm"` to
get a **plan** instead — the injected SQL plus one presigned Parquet per dataset —
and execute it yourself (in DuckDB-WASM in a browser, or plain DuckDB locally):

```python
result = client.query(sql="SELECT region, COUNT(*) FROM sales GROUP BY region", type="wasm")

if result.mode == "wasm":
    for src in result.plan.sources:
        print(src.alias, src.url, src.bytes)   # register each under `alias`
    print(result.plan.sql)                     # then run this
else:
    print(result.rows, result.fallback_reason) # ran server-side, and why
```

**Authorization does not move.** The server still authorizes, validates
alias-only SQL, and injects row filters — in that order, *before* the capability
check. Asking for wasm skips no gate; you get a plan only for data you were
already allowed to read.

**Asking for wasm does not guarantee getting it.** If any participating dataset
isn't wasm-capable — no current extract, an extract over the size cap, or a
mandatory row-level filter — the query runs server-side and the response says so
via `mode: "backend"` and a `fallback_reason`. It is never a silent downgrade, so
**always branch on `result.mode`, never on what you requested.**

`VibedasherEmbedClient` (scoped-token) has no wasm lane and raises `ValueError`
rather than quietly serving rows.

### Embed vs eject

- **Embed (iframe, zero-code):** paste a snippet; we host and render. Nothing to build.
- **Eject (this SDK, own-your-code):** pull the dashboard's code into your stack and
  feed it data via `client.query(...)`. Your app owns the rendering; only the data
  crosses the wire. See a runnable host app in `examples/nextjs-embed` (TS), the
  same contract as here.

### Client-side (no backend) auth

For a pure-frontend app, mint a short-TTL **scoped token** and use the same method
via the token-pinned client:

```python
from vibedasher import VibedasherEmbedClient

client = VibedasherEmbedClient(token=scoped_token, region="eu-central-1")
result = client.query(sql=sql, params=params)
# The token pins the viz + datasets — dataset_ids are accepted for symmetry but the
# server enforces the token's bound set.
```

## Everything else: resource operations (generated)

Datasets, uploads, viz metadata, API keys, etc. are generated from the OpenAPI spec:

```python
from vibedasher.factory import create_client
from vibedasher.api.datasets import read_datasets_v1_datasets_get

client = create_client(api_key="sk_...", region="eu-central-1")
# Each operation lives under vibedasher.api.<tag>.<operation_id> and exposes
# .sync(), .sync_detailed(), .asyncio(), .asyncio_detailed().
datasets = read_datasets_v1_datasets_get.sync(client=client)
```

Auth is an API key sent as `X-Api-Key` (mint one via `POST /v1/api-keys`).

## Layout

- `vibedasher/_query.py` — the hand-written `query()` (transport, decode, retry, typed rows).
- `vibedasher/api/<tag>/<operation>.py` — one module per API operation.
- `vibedasher/models/` — request/response models (attrs classes).
- `vibedasher/factory.py` — `create_client(api_key, region=..., base_url=...)`.

> **Generated code** — do not edit `vibedasher/` by hand. Regenerate with `make py`
> from `packages/sdk/`. Source of truth: `../openapi.json`. `query()` is the
> deliberate exception — hand-written and re-copied after each generation.
