Metadata-Version: 2.4
Name: betflux
Version: 0.1.1
Summary: Python client + CLI for the BetFlux data API — normalized sportsbook odds for sharp bettors.
Author: BetFlux
License-Expression: MIT
Project-URL: Homepage, https://betflux.ai
Project-URL: Documentation, https://betflux.ai/developer
Keywords: sportsbook,betting,odds,sports-data,parquet,api-client
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Operating System :: OS Independent
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.27
Requires-Dist: click>=8.1
Requires-Dist: pyarrow>=16.0
Provides-Extra: pandas
Requires-Dist: pandas>=2.0; extra == "pandas"
Provides-Extra: polars
Requires-Dist: polars>=1.0; extra == "polars"
Provides-Extra: test
Requires-Dist: pytest>=8.0; extra == "test"
Requires-Dist: pandas>=2.0; extra == "test"
Dynamic: license-file

# betflux

Python client + CLI for the [BetFlux](https://betflux.ai) data API — normalized
sportsbook odds across US books, queryable for sharp bettors.

## Install

```bash
pip install betflux            # core client + CLI
pip install "betflux[pandas]"  # add .df() DataFrames
```

## Authenticate

Set your API key (mint one at `betflux.ai/account/api-keys`):

```bash
export BETFLUX_API_KEY=bfx_live_...
```

## The file model

Every dataset payload is a **per-game Parquet file** served at
`GET /v1/games/{game_id}/{dataset}` — the server streams typed, compressed
bytes (with HTTP Range support) and never filters. The SDK/CLI keep their
query interfaces: they discover game ids via `/v1/games`, download each
game's file, and evaluate your filters locally with pyarrow (a core
dependency). Quota is metered as rows *downloaded*, so narrow date ranges
and `--game` fetches are the cheap path — local filters don't reduce spend.

Datasets: `closing-lines`, `market-results` (graded closing lines),
`sportsbook-lines` (the full ~190k-row change-only line history per game),
`game-state-timeline` (flat `ts/field/value/source` observation rows).

## CLI

```bash
betflux keys check
betflux datasets
betflux games --league NBA --date-from 2026-04-01 --date-to 2026-04-07
betflux get closing-lines --league NBA --date-from 2026-04-01 --date-to 2026-04-07
betflux get closing-lines --game NBA_GSW_MIA_20260401 --format jsonl
betflux get market-results --league MLB --date-from 2026-07-01 --date-to 2026-07-07 --outcome WON
betflux get sportsbook-lines --game MLB_BOS_NYY_20260715 --output lines.parquet
betflux get game-state-timeline --game NBA_GSW_MIA_20260401
betflux leagues
betflux teams --league NBA
betflux players --team-id <team-id>
```

Game ids are readable — `LEAGUE_AWAY_HOME_YYYYMMDD` (ET date; a `_2` suffix
marks doubleheaders), case-insensitive, discoverable via `betflux games`.
Internal UUIDs are accepted everywhere a game id is.

`closing-lines` and `market-results` accept a date range or `--game`;
`game-state-timeline` and `sportsbook-lines` are `--game` only
(sportsbook-lines over a range would debit ~190k quota rows per game).
Filters (`--operator`, `--market-type`, `--team`, `--player-id`, `--side`,
`--outcome`) run locally after download; a filter the dataset has no column
for is rejected up front. `--limit N` stops fetching once N rows have been
yielded. With `--format jsonl|csv` rows stream as each game's file arrives.

`--output PATH` (with `--game`) saves the raw Parquet file for any dataset —
no parsing, one summary line with the row count:

```
wrote lines.parquet — sportsbook-lines for MLB_BOS_NYY_20260715: 190,412 rows, 8,214,567 bytes
```

Timestamp columns are real Parquet timestamps and render as ISO 8601 in
every output format.

Output defaults to a compact table showing a curated column subset (the gold
datasets are wide). Widen or reshape it:

- `--wide` — every column in the table
- `--columns game_date,operator,side,closing_odds` — pick columns
- `--format record` — vertical `key: value` blocks, ideal for one wide row
- `--format json|jsonl|csv` — machine formats (always full-fidelity)

`betflux keys check` validates the key and reports your plan:

```
key valid (https://api.betflux.ai)
tier: Beta — 120 requests/min
usage: 12,345 / 50,000 rows this month (25%) — resets 2026-08-01
```

## Library

```python
from betflux import Client

with Client() as bf:
    for row in bf.closing_lines.iter(
        league="NBA", date_from="2026-04-01", date_to="2026-06-30",
        operator="FANDUEL",  # local filters: operator/market_type/team/player_id/side/outcome
        max_rows=10_000,     # stop fetching once this many rows yielded; default None = all
    ):
        print(row["market_key"], row["closing_odds"])

    games = bf.games(league="NBA", date_from="2026-04-01", date_to="2026-04-07")
    df = bf.closing_lines.df(league="NBA", date_from="2026-04-01", date_to="2026-04-07")
    game_rows = bf.market_results.game(games[0]["id"], outcome="WON")
    observations = bf.state_timeline(games[0]["id"])  # flat rows; ts is epoch-ms
    raw = bf.sportsbook_lines.raw(games[0]["id"])     # the Parquet bytes, verbatim
```

Datasets hang off the client as `closing_lines`, `market_results`,
`sportsbook_lines`, and `game_state_timeline` (or `bf.dataset("closing-lines")`
by public name). Timestamp/date columns come back as Python `datetime`/`date`
objects (pyarrow decodes the Parquet types), **not** ISO strings. Filters that
name a column the dataset lacks raise `ValueError`; `team` matches home or
away, `player_id` matches the market/selection player-id list columns.

Errors are typed (`AuthError`, `PaymentRequiredError`, `RateLimitError`,
`QuotaExceededError`, `NetworkError`, …); rate limits, 5xx, and transport
failures retry automatically with backoff that honors `Retry-After` (capped).

### Breaking changes (Parquet cutover)

- `iter()` lost `page_size` — there is no server pagination to tune anymore.
- `Client.state_timeline()` returns the flat observation rows
  (`list[dict]`) instead of the old `{..., observations: [...]}` envelope.
- Timestamps in rows are `datetime` objects, not ISO strings.

### Power user: DuckDB straight at the files

The dataset endpoints are plain authenticated Parquet URLs with Range
support, so DuckDB can query them directly — predicate pushdown means it
reads only the byte ranges it needs:

```sql
CREATE SECRET betflux (
    TYPE http,
    EXTRA_HTTP_HEADERS MAP {'Authorization': 'Bearer bfx_live_...'}
);
SELECT operator, market_type, odds, timestamp
FROM read_parquet('https://api.betflux.ai/v1/games/MLB_BOS_NYY_20260715/sportsbook-lines')
WHERE market_type = 'MONEYLINE'
ORDER BY timestamp;
```

Note: quota is debited for the file's full row count per request, partial
read or not.
