Metadata-Version: 2.5
Name: vibedasher
Version: 2.3.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
Requires-Dist: typing-extensions>=4.0.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.
```

### Concurrency: a 503 is a limit, not an outage

The query engine has a finite, shared concurrency ceiling. A `503 Service
Unavailable` from `query()` means the request was throttled, not that the service
is down — the same SQL run serially succeeds. `query()` retries throttled
requests (429/502/503/504) with jittered backoff and honours `Retry-After`.

`httpx.Client` is synchronous, so one client is already one query at a time and
there is no in-flight gate here — that lives in the TypeScript SDK, where a
browser dashboard fans out on mount. If you thread this client yourself, keep the
width modest.

Errors raise `QueryHttpError` (a `httpx.HTTPStatusError` subclass, so existing
`except` clauses keep working) carrying `status_code`, the API's own message, and
`request_id` — quote that in a bug report.

## 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`).

### Paging a list endpoint: `iter_all()`

Every cursor-paginated list endpoint returns one page plus a `nextKey`:

```json
{ "datasets": [ ... ], "nextKey": 812 }
```

`nextKey: null` means that was the last page — it is the **only** end-of-list
signal. Rather than hand-rolling that loop, iterate:

```python
for dataset in client.datasets.iter_all():
    print(dataset.name)                    # pages transparently, rows streamed

for dataset in client.datasets.iter_all(page_size=50, max_pages=10, folder_id=7):
    ...                                    # cap the work; extra kwargs are forwarded

for page in client.datasets.iter_pages():
    print(page.next_key)                   # page-at-a-time, e.g. to checkpoint
```

Nested collections take the method name:

```python
for upload in client.datasets.iter_all(method="list_uploads", dataset_id=4):
    ...
```

Notes:

- **Bounded memory** — rows are yielded as each page arrives, and `break` stops
  the requests immediately. Nothing is prefetched or accumulated.
- **`max_pages`** caps the work: iterating an unbounded table to exhaustion is a
  footgun, so the ceiling is one keyword away.
- **A repeated cursor raises `CursorNotAdvancingError`** instead of looping
  forever. That is a server bug in the endpoint's `nextKey`; the SDK surfaces it
  rather than absorbing it as a short read.
- For a paginated endpoint that is not on the resource, the standalone form
  works on any list callable: `from vibedasher import iter_all`.

## Layout

- `vibedasher/_query.py` — the hand-written `query()` (transport, decode, retry, typed rows).
- `vibedasher/_pagination.py` — the hand-written `iter_all()` / `iter_pages()` cursor loop.
- `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()` and the
> pagination iterators are the deliberate exceptions — hand-written in
> `packages/sdk/py/{_query,_pagination}.py` and re-copied after each generation.
