Metadata-Version: 2.3
Name: bulletins-sdk
Version: 0.3.0
Summary: Python SDK for Bulletins
Author: Fredrik Angelsen
Author-email: Fredrik Angelsen <fredrikangelsen@gmail.com>
Requires-Dist: httpx>=0.28.1
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# bulletins

Python SDK for the [Bulletins](https://bulletins.no) API. Async client, SSE streaming, and a declarative bot framework.

## Install

```bash
uv add bulletins-sdk
```

## Quick start

### Client

```python
import asyncio
from bulletins import BulletinsClient

async def main():
    client = BulletinsClient("bk_...", base_url="https://bulletins.no")

    async with client:
        await client.resolve()  # auto-resolves party + integration from key
        threads = await client.list_threads()
        for t in threads:
            print(f"{t.name} ({t.status})")

        await client.send_message(threads[0].id, "Hello from Python")

asyncio.run(main())
```

### SSE streaming

```python
async with client.stream_party() as stream:
    async for event in stream:
        print(f"[{event['type']}] {event.get('senderName')}: {event['content']}")
```

### Bot

Declarative bot with auto-registration of event types and actions:

```python
import asyncio
from bulletins import Bot, EventTypeSpec, ActionSpec

bot = Bot(
    "Ping Bot",
    event_types=[
        EventTypeSpec("bot:pong",
            content_schema={"message": {"type": "string"}},
            display_hint={"label": "Pong"}),
    ],
    actions=[
        ActionSpec("Approve", color="success"),
        ActionSpec("Reject", color="destructive"),
    ],
)

@bot.on("message")
async def handle(event, client):
    body = (event.content or {}).get("body", "")
    if body.startswith("!ping"):
        await client.send_message(event.thread_id, "pong")

@bot.on("action_invoked")
async def on_action(event, client):
    key = (event.content or {}).get("key", "")
    match key:
        case "Approve": ...
        case "Reject": ...

asyncio.run(bot.start(api_key="bk_...", base_url="https://bulletins.no"))
```

Create the integration and API key in the Bulletins settings UI. The bot auto-resolves its party and integration from the API key, registers event types and actions on startup, and dispatches incoming events to handlers.

Action IDs are available via `bot.action_ids` after start — pass them to `create_event` to attach inline controls:

```python
await client.create_event(thread_id, "bot:pong", {"message": "pong"},
    actions=[bot.action_ids["Approve"], bot.action_ids["Reject"]])
```

## API

### `BulletinsClient`

| Method | Description |
|---|---|
| `resolve()` | Auto-resolve party + integration from API key |
| `list_threads()` | List threads for the active party |
| `get_thread(id)` | Thread detail with events and participants |
| `create_thread(name, to_party_id, body)` | Create a thread with initial message |
| `list_events(thread_id, before, limit)` | Paginated event history |
| `create_event(thread_id, type, content, actions)` | Create any event type, optionally with inline action IDs |
| `send_message(thread_id, body)` | Send a text message |
| `stream_party()` | SSE stream for all party events |
| `stream_thread(thread_id)` | SSE stream for a single thread |
| `list_integrations()` | List integrations |
| `get_integration_detail(id)` | Integration with all child resources |
| `create_event_type(integration_id, ...)` | Register a custom event type |
| `create_action(integration_id, ...)` | Create an action |
| `create_webhook(integration_id, ...)` | Create a webhook |
| `create_api_key(integration_id, ...)` | Create an API key |
| `get_agent_token(thread_id)` | LiveKit token for joining calls |

### `Bot`

| Method | Description |
|---|---|
| `on(event_type)` | Decorator to register an event handler |
| `start(api_key, base_url)` | Resolve identity, register event types + actions, dispatch loop |
| `stop()` | Stop the dispatch loop |
| `action_ids` | `dict[str, str]` — action name to UUID, populated after start |

### `ActionSpec`

| Field | Type | Description |
|---|---|---|
| `name` | `str` | Action button label |
| `method` | `str` | `"event"` (default) or `"POST"` |
| `color` | `str \| None` | Semantic token: `"destructive"`, `"success"`, or `"warning"` |
| `requested_schema` | `dict \| None` | JSON Schema for form/slider input |
| `url_template` | `str \| None` | URL template for POST/link actions |

### `EventTypeSpec`

| Field | Type | Description |
|---|---|---|
| `name` | `str` | Namespaced type, e.g. `"bot:now_playing"` |
| `content_schema` | `dict` | JSON Schema for event content validation |
| `display_hint` | `dict \| None` | `{"label": "Human-readable name"}` |

## Requirements

Python 3.12+, httpx.
