Metadata-Version: 2.4
Name: sidekick-ads
Version: 0.4.0
Summary: Python SDK for the Sidekick ad-injection API
License-Expression: MIT
Requires-Python: >=3.9
Requires-Dist: httpx>=0.25.0
Provides-Extra: aiogram
Requires-Dist: aiogram>=3.0; extra == 'aiogram'
Provides-Extra: ptb
Requires-Dist: python-telegram-bot>=20.0; extra == 'ptb'
Description-Content-Type: text/markdown

# sidekick-ads

Python SDK for the Sidekick ad-injection API.

## Installation

```bash
# Core (httpx only)
pip install sidekick-ads

# With aiogram support
pip install sidekick-ads[aiogram]

# With python-telegram-bot support
pip install sidekick-ads[ptb]
```

## Quick start

### aiogram 3.x with middleware

```python
from aiogram import Bot, Dispatcher, Router
from aiogram.types import Message
from sidekick_ads import Sidekick
from sidekick_ads.middleware import SidekickMiddleware

bot = Bot(token="BOT_TOKEN")
dp = Dispatcher()
router = Router()
dp.include_router(router)

# Register the middleware — it injects a Sidekick instance into every handler
dp.message.middleware(
    SidekickMiddleware(api_key="sk_xxx", platform_id="plt_xxx")
)


@router.message()
async def handle(message: Message, sidekick: Sidekick):
    llm_response = "Here is your answer..."

    user = message.from_user
    user_id = user.id if user else 0
    language_code = user.language_code if user else None
    is_premium = user.is_premium if user else None

    result = await sidekick.inject(
        user_id=user_id,
        message=llm_response,
        language_code=language_code,
        is_premium=is_premium,
        keyboard=None,  # or an existing InlineKeyboardMarkup
    )

    await message.answer(
        result.message,
        reply_markup=result.keyboard,
    )
```

### aiogram 3.x without middleware

```python
from aiogram import Bot, Dispatcher, Router
from aiogram.types import Message
from sidekick_ads import Sidekick

bot = Bot(token="BOT_TOKEN")
dp = Dispatcher()
router = Router()
dp.include_router(router)

sidekick = Sidekick(api_key="sk_xxx", platform_id="plt_xxx")


@router.message()
async def handle(message: Message):
    llm_response = "Here is your answer..."

    user = message.from_user
    user_id = user.id if user else 0
    language_code = user.language_code if user else None
    is_premium = user.is_premium if user else None

    result = await sidekick.inject(
        user_id=user_id,
        message=llm_response,
        language_code=language_code,
        is_premium=is_premium,
        keyboard=None,  # or an existing InlineKeyboardMarkup
    )

    await message.answer(
        result.message,
        reply_markup=result.keyboard,
    )
```

### python-telegram-bot v20+

```python
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import ApplicationBuilder, CommandHandler, ContextTypes
from sidekick_ads import Sidekick

sidekick = Sidekick(api_key="sk_xxx", platform_id="plt_xxx")


async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    llm_response = "Here is your answer..."

    user = update.effective_user
    user_id = user.id if user else 0
    language_code = user.language_code if user else None
    is_premium = user.is_premium if user else None

    # Example: existing keyboard with one button
    existing_kb = InlineKeyboardMarkup(
        [[InlineKeyboardButton(text="My button", url="https://example.com")]]
    )

    result = await sidekick.inject(
        user_id=user_id,
        message=llm_response,
        language_code=language_code,
        is_premium=is_premium,
        keyboard=existing_kb,
    )

    await update.message.reply_text(
        result.message,
        reply_markup=result.keyboard,
    )


app = ApplicationBuilder().token("BOT_TOKEN").build()
app.add_handler(CommandHandler("start", start))
app.run_polling()
```

### Raw usage (no framework)

```python
import asyncio
from sidekick_ads import Sidekick


async def main():
    sidekick = Sidekick(api_key="sk_xxx", platform_id="plt_xxx")

    result = await sidekick.inject(
        user_id=123456789,
        message="Here is your answer...",
        language_code="en",
        is_premium=False,
    )

    print(result.message)
    print(result.has_ad)
    print(result.impression_id)
    print(result.ad)
    print(result.keyboard)

    await sidekick.close()


asyncio.run(main())
```

## `fetch()` — privacy-friendly alternative

If your LLM reply contains user data (names, query fragments) that you'd
prefer not to send to Sidekick, use `fetch()` instead of `inject()`. It
returns the ad as separate fields, and you render them into your reply
yourself.

```python
import asyncio
from sidekick_ads import Sidekick


async def main():
    sidekick = Sidekick(api_key="sk_live_xxx", platform_id="plt_xxx")

    result = await sidekick.fetch(
        user_id=123456789,
        language_code="en",
        parse_mode="HTML",  # optional — adds `ad_text_formatted`
    )

    llm_reply = "Here is your answer..."

    if result.has_ad and result.ad is not None:
        text = f"{llm_reply}\n\n{result.ad.ad_text_formatted}"
        keyboard = {"inline_keyboard": [[
            {"text": result.ad.button_text, "url": result.ad.button_url}
        ]]}
        # send `text` with `keyboard` via your bot framework
    else:
        # send `llm_reply` unchanged
        pass

    await sidekick.close()


asyncio.run(main())
```

### `await sidekick.fetch(...) -> FetchResult`

| Parameter       | Type              | Default    |
|-----------------|-------------------|------------|
| `user_id`       | int               | required   |
| `language_code` | str               | required   |
| `is_premium`    | bool \| None      | `None`     |
| `parse_mode`    | str \| None       | `None`     |

### `FetchResult` and `FetchAdData`

```python
@dataclass
class FetchAdData:
    ad_text: str
    ad_url: str
    button_text: str
    button_url: str
    ad_text_formatted: Optional[str] = None  # only when parse_mode is sent


@dataclass
class FetchResult:
    has_ad: bool
    impression_id: Optional[str]
    ad: Optional[FetchAdData]
```

Like `inject()`, `fetch()` never raises — on any error (timeout, network
failure, 5xx) it returns `FetchResult(has_ad=False, impression_id=None, ad=None)`.

See the [`/api/v1/ad/fetch` HTTP endpoint](https://sidekick-ads.com/docs)
for the full wire format.

## API reference

### `Sidekick(api_key, platform_id, *, base_url, timeout, platform)`

| Parameter     | Type   | Default                            |
|---------------|--------|------------------------------------|
| `api_key`     | str    | required                           |
| `platform_id` | str   | required                           |
| `base_url`    | str    | `https://sidekick-ads.com`    |
| `timeout`     | float  | `3.0`                              |
| `platform`    | str    | `"telegram"`                       |

### `await sidekick.inject(...) -> InjectResult`

| Parameter           | Type              | Default    |
|---------------------|-------------------|------------|
| `user_id`           | int               | required   |
| `message`           | str               | required   |
| `language_code`     | str \| None       | `None`     |
| `is_premium`        | bool \| None      | `None`     |
| `keyboard`          | Any \| None       | `None`     |
| `ad_button_position`| str               | `"bottom"` |

### `InjectResult`

| Field           | Type          |
|-----------------|---------------|
| `message`       | str           |
| `has_ad`        | bool          |
| `impression_id` | str \| None   |
| `ad`            | dict \| None  |
| `keyboard`      | Any \| None   |

### `await sidekick.fetch(...) -> FetchResult`

| Parameter       | Type              | Default    |
|-----------------|-------------------|------------|
| `user_id`       | int               | required   |
| `language_code` | str               | required   |
| `is_premium`    | bool \| None      | `None`     |
| `parse_mode`    | str \| None       | `None`     |

### `FetchResult`

| Field           | Type                    |
|-----------------|-------------------------|
| `has_ad`        | bool                    |
| `impression_id` | str \| None             |
| `ad`            | `FetchAdData` \| None   |

### `FetchAdData`

| Field               | Type          |
|---------------------|---------------|
| `ad_text`           | str           |
| `ad_url`            | str           |
| `button_text`       | str           |
| `button_url`        | str           |
| `ad_text_formatted` | str \| None   |

### `await sidekick.close()`

Closes the underlying HTTP connection pool. The client is automatically
recreated on the next `inject()` call if needed.
