Metadata-Version: 2.4
Name: streamformation-sdk
Version: 0.1.0
Summary: Python SDK for StreamFormation
Project-URL: Homepage, https://streamformation.com
Project-URL: Repository, https://git.nedos.co/streamformation/sf-mono
Requires-Python: >=3.11
Requires-Dist: httpx>=0.28.1
Requires-Dist: websockets>=15.0.1
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: ruff>=0.3.0; extra == 'dev'
Description-Content-Type: text/markdown

# StreamFormation Python SDK

`sdk/` is the Python client for StreamFormation. It is used by the CLI, smoke
tests, and any Python integration that needs authenticated API resources, alert
submission, media management, or voice audio generation.

## Install

From the repository root:

```bash
python3 -m pip install -e ./sdk
```

The SDK reads configuration from explicit constructor arguments first, then from
environment variables, then from `$HOME/.sf/config`, then from the nearest
project `.sf/config`.

| Variable | Purpose |
| --- | --- |
| `STREAMFORMATION_API_URL` / `SF_API_URL` | API base URL, including `/api`. Defaults to production. |
| `STREAMFORMATION_API_KEY` / `SF_API_KEY` | User bearer token or user key. |
| `STREAMFORMATION_ADMIN_API_KEY` / `SF_ADMIN_API_KEY` | Admin key for bootstrap/test operations. |
| `STREAMFORMATION_ALERT` / `SF_ALERT` | Default alert used by CLI shorthand and local agent scripts. |
| `STREAMFORMATION_REFERENCE_AUDIO` / `SF_REFERENCE_AUDIO` | Default reference audio path used by CLI shorthand voice-clone sends. |

Agents can keep default config in `$HOME/.sf/config`; project-local `.sf/config`
is a fallback when a value is missing from home config:

```text
STREAMFORMATION_API_URL=https://api.dev.streamformation.com/api
STREAMFORMATION_API_KEY=usr_... or user JWT
STREAMFORMATION_ALERT=alert-1
STREAMFORMATION_REFERENCE_AUDIO=/absolute/path/to/reference.wav
```

Dev URL helper:

```python
from streamformation import StreamFormation

sf = StreamFormation(dev=True, api_key="...")
```

## Client Layout

`StreamFormation` is an alias of `StreamFormationClient`.

```python
from streamformation import StreamFormation

sf = StreamFormation(
    api_url="https://api.dev.streamformation.com/api",
    api_key="user jwt or user key",
    admin_api_key="admin key",
)
```

Resources:

| Resource | Purpose |
| --- | --- |
| `sf.auth` | Login through the API auth routes. |
| `sf.user` | Current profile and user key rotation. |
| `sf.config` | User alert/payment configuration. |
| `sf.alerts` | Authenticated alert CRUD and key rotation under `/api`. |
| `sf.alert_push` | Submit alert notifications with `key_...` root routes. |
| `sf.alert_pull` | Build read-side WebSocket URLs with `alert_...`. |
| `sf.media` | Upload/list/download/delete media. |
| `sf.voice` | Generate TTS, cloned, or designed voice audio. |
| `sf.admin` | Admin bootstrap/login/delete helpers. |

## Alert Keys and URLs

Both SDKs use one API base URL. The SDK derives the root alert routes from it:

| Key | Use | SDK |
| --- | --- | --- |
| `key_...` | Write-side submit/trigger key. | `AlertPush.send()` / `AlertPush.trigger()` |
| `alert_...` | Read-side overlay/WebSocket key. | `AlertPull.websocket_url()` |

Example:

```python
from streamformation import AlertPull, AlertPush, ArbitraryNotification

push = AlertPush("https://api.dev.streamformation.com/api")
pull = AlertPull("https://api.dev.streamformation.com/api")

result = push.send(
    "key_...",
    ArbitraryNotification.create(data={"message": "hello", "amount": 42}),
)
print(result.status, result.deliveries)
print(pull.websocket_url("alert_..."))
```

## Alerts

```python
alert = sf.alerts.create(
    name="Donation",
    short_url="donation",
    description="Donation overlay",
    enabled=True,
    config={"type": "donation", "layout": "bottom"},
)

print(alert.id, alert.key, alert.alert_key)
print(alert.urls.submit)
print(alert.urls.websocket)

alert = sf.alerts.update(alert.id, description="Updated")
new_submit_key = sf.alerts.rotate_key(alert.id)
new_read_key = sf.alerts.rotate_alert_key(alert.id)
sf.alerts.delete(alert.id)
```

## Notification Types

Notification classes are small payload builders. They serialize to:

```json
{"meta": {}, "data": {}}
```

Plain arbitrary data:

```python
from streamformation import ArbitraryNotification

notification = ArbitraryNotification.create(
    meta={"source": "python"},
    data={"message": "Thanks", "amount": 42},
    settings={"duration": 2500},
)
sf.alert_push.send("key_...", notification)
```

Caller-formatted alert data:

```python
from streamformation import FormattedNotification

notification = FormattedNotification.create(
    formatted={
        "title": "New member",
        "body": "Ada joined",
        "accent": "#19c37d",
    },
    settings={"animationIn": "fadeInUp"},
)
sf.alert_push.send("key_...", notification)
```

Dynamic audio/media:

```python
from streamformation import DynamicAudioNotification

notification = DynamicAudioNotification.create(
    message="Thanks for the donation",
    audio_url="https://media-dev.streamformation.com/usr_.../voice/clip.mp3",
    media_url="https://media-dev.streamformation.com/usr_.../alert.gif",
    settings={"soundVolume": 0.7, "duration": 2500},
)
sf.alert_push.send("key_...", notification)
```

Backend-rendered voice clone for one notification:

```python
result = sf.alert_push.send_with_reference_audio(
    "key_...",
    message="Thanks for the donation",
    ref_audio=".sf/reference.wav",
    data={"amount": "10.00"},
    settings={"duration": 2500},
)
print(result.notification_id)
print(result.notification["data"]["audio_url"])
```

This submit-key path does not require user auth. The backend stores generated
audio as `render/{notification_id}/voice-clone.mp3`; it is not added to the
user media library. For async tracking, pass `wait=False` and poll:

```python
queued = sf.alert_push.send_with_reference_audio(
    "key_...",
    message="Thanks",
    ref_audio=".sf/reference.wav",
    wait=False,
)
status = sf.alert_push.status(queued["id"])
```

`settings` is optional on every notification type. It overrides the saved alert
builder settings for that single render, for fields such as `duration`,
`animationIn`, `animationOut`, `layout`, `soundVolume`, and text block config.
For backend-rendered reference audio, `duration` defaults to the generated audio
length plus backend padding unless you set `settings.duration` explicitly.

## Voice

Voice calls return StreamFormation media objects. The backend calls Hyper voice
services, stores the returned audio in media, and returns a public URL.

TTS:

```python
audio = sf.voice.tts(
    "Thanks for the donation",
    voice="Chelsie",
    language="auto",
    response_format="mp3",
)
print(audio["url"])
```

Clone:

```python
audio = sf.voice.clone(
    "Thanks for the donation",
    ref_audio="tests/fixtures/voice/reference.wav",
    response_format="mp3",
)
```

Design:

```python
audio = sf.voice.design(
    "Welcome in",
    description="warm narrator, upbeat, clean studio voice",
    response_format="mp3",
)
```

Send generated audio to an alert:

```python
from streamformation import DynamicAudioNotification

audio = sf.voice.clone("Thanks!", ref_audio="./reference.wav")
sf.alert_push.send(
    "key_...",
    DynamicAudioNotification.create(message="Thanks!", audio_url=audio["url"]),
)
```

Typical response:

```json
{
  "operation": "clone",
  "bucket": "media",
  "key": "usr_.../voice/....mp3",
  "url": "https://media-dev.streamformation.com/usr_.../voice/....mp3",
  "content_type": "audio/mp3",
  "size": 45716,
  "response_format": "mp3",
  "source": {}
}
```

## Media

```python
media = sf.media.upload("./overlay.png")
print(media["key"], media["url"])

items = sf.media.list(limit=100)
sf.media.download(media["key"], "./overlay.png")
raw = sf.media.download_bytes(media["key"])
sf.media.delete(media["key"])
```

## Admin and E2E Bootstrap

Admin helpers require `admin_api_key`.

```python
user = sf.admin.ensure_user(
    uid="dev-user",
    handle="dev-user",
    email="dev-user@example.com",
    name="Dev User",
)
token = sf.admin.login(user.id)
user_sf = sf.with_token(token)
print(user_sf.user.get())
sf.admin.delete_user(user.id)
```

## x402

The Python SDK intentionally does not wrap browser wallet payment flows. Use the
TypeScript SDK `X402PaymentClient` for wallet-backed x402 calls because it owns
the browser-side x402 axios interceptor.

## Testing

```bash
python3 -m pip install -e ./sdk
python3 -m pytest sdk/tests
```
