Metadata-Version: 2.4
Name: keeps-sdk
Version: 0.3.0
Summary: Media-generation observability SDK — capture the assets your app generates as events
License: Proprietary
Requires-Python: >=3.11
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# keeps — media-generation observability SDK (v0.3.0)

> **User-facing docs live in [docs/](docs/introduction.mdx)** (Mintlify site:
> quickstart, core concepts, providers, reliability, privacy, API + HTTP
> reference); their design rationale is in
> [../spec/DOCS-RATIONALE.md](../spec/DOCS-RATIONALE.md). This README is the
> engineering-facing summary.

Standalone and provider-agnostic — the SDK never wraps or patches
`fal_client` / `replicate` / `openai`. You call `capture()` around your own
provider code; the only thing carried across processes is the client-owned
`asset_id`. The wire contract it emits is **MVP-SPEC §3**
([../spec/MVP-SPEC.md](../spec/MVP-SPEC.md)) — envelope changes go there
first (invariant 8).

```python
import keeps

keeps.init(api_key="kp_live_…")   # or KEEPS_API_KEY / KEEPS_INGEST_URL env vars

# First call mints and returns a uuid7 asset_id; reuse it to append events.
asset_id = keeps.capture(
    model="fal-ai/flux-pro/v1.1",        # bare provider-native id
    provider="fal",                      # dimension for pricing — never a join key
    status="submitted",
    inputs={"prompt": "an elephant"},
)
# … your real provider call runs, untouched …
keeps.capture(
    asset_id,
    status="completed",
    outputs=[{"url": url}],              # references, never bytes
    usage={"n": 1, "duration_s": 6.2},   # provider billing meters, verbatim
    properties={"generation_id": request_id, "tier": "pro"},
)
```

Full surface: `init`, `capture`, `context` (ambient tags), `session`
(tags + flush-on-exit), `flush`, `shutdown`, `stats`.

## Design guarantees

- **Fail-open, always.** The SDK cannot raise into your code, cannot block
  beyond an in-memory append, and drops its own events under overflow or
  outage. Killing the ingest endpoint does not touch your pipeline — there
  are tests proving this.
- **Stateless.** No files, no local DB. One bounded in-memory buffer and a
  daemon flusher thread (fork-aware for prefork servers).
- **References, not bytes.** Events carry output URLs + metadata. Media
  never leaves your infrastructure through this SDK; `bytes` values are
  reduced to a length stub.
- **Verbatim capture.** `inputs` are recorded as-is for maximum fidelity — the
  SDK does not redact. Redact any sensitive field yourself before `capture()`.
- **Truncation is byte-based and flagged out of band** (envelope `truncated`
  array). Over-budget `inputs` collapse to a hashed stub; `properties` trim
  values (never a stub key); `outputs` keep the first refs plus a
  `{"__dropped__": n}` marker.
- **Stdlib-only runtime.** Zero dependencies.

## Transport

Batches (≤200 events) POST to `/v1/events` with a top-level `sent_at` (lets
ingest cancel client clock skew), gzipped ≥1 KiB. Retries: 3× jittered
backoff; `429 Retry-After` honored; `413` bisects the batch so one oversized
event costs only itself; other 4xx drop with a one-time warning. The 202 body
(`{"accepted": n, "rejected": [...]}`) is parsed into `stats()`.

## Configuration

| `init()` arg     | env var           | default                                  |
|------------------|-------------------|------------------------------------------|
| `api_key`        | `KEEPS_API_KEY`    | — (unset ⇒ SDK disabled, warns)          |
| `endpoint`       | `KEEPS_INGEST_URL` | unreachable `.invalid` placeholder       |
| `disabled`       | `KEEPS_DISABLED`   | `False` (`1`/`true`/`yes` disables — CI) |
| `user_id`        |                   | `None` (process default; override per call or via `keeps.context()`) |
| `flush_interval` |                   | `1.0`s                                   |
| `max_buffer`     |                   | `10_000` events                          |

## Observability of the observer

`keeps.stats()` → `initialized`, `disabled`, `emitted`, `sent`, `batches`,
`retries`, `dropped_overflow`, `dropped_send`, plus the server's own ledger
from each 202 body: `accepted` (stored) and `rejected_by_server` (per-event
rejects — terminal, logged once with reason codes).

## Development

```bash
uv venv && uv pip install -e ".[dev]"
pytest                  # full suite, incl. fail-open + concurrency tests
ruff check . && ruff format --check .
```

The wire contract lives in [../spec/MVP-SPEC.md](../spec/MVP-SPEC.md) §3 —
schema changes go there first. Judgment calls are logged in
[../spec/DECISIONS.md](../spec/DECISIONS.md).
