Metadata-Version: 2.4
Name: nevo-sdk
Version: 0.1.3
Summary: Python SDK for Nevo — the inbound event gateway.
Project-URL: Homepage, https://nevo.sh
Project-URL: Documentation, https://docs.nevo.sh/python
Project-URL: Repository, https://github.com/nevo-sh/sdk-python
Project-URL: Issues, https://github.com/nevo-sh/sdk-python/issues
Author: Nevo
License: MIT
License-File: LICENSE
Keywords: agents,ai,events,webhooks,websocket
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
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: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: httpx<0.29,>=0.27
Requires-Dist: websockets<14.0,>=12.0
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# Nevo Python Library

[![PyPI version](https://img.shields.io/pypi/v/nevo-sdk.svg)](https://pypi.org/project/nevo-sdk/)
[![Python versions](https://img.shields.io/pypi/pyversions/nevo-sdk.svg)](https://pypi.org/project/nevo-sdk/)
[![License](https://img.shields.io/pypi/l/nevo-sdk.svg)](https://pypi.org/project/nevo-sdk/)

The official Python library for [Nevo](https://nevo.sh) — one inbound layer for your backend. Webhooks, emails, and more land on a single handler, signature-verified and ready to process. Works the same whether the handler is a traditional service or an AI agent reading the world and replying.

## Documentation

See the [Python quickstart](https://docs.nevo.sh/sdk/python/quickstart) and [events reference](https://docs.nevo.sh/concepts/events).

## Requirements

- Python 3.10+

## Installation

```sh
pip install nevo-sdk
```

The import path stays `nevo` — only the distribution name carries the `-sdk` suffix:

```python
from nevo import Nevo
```

## Usage

You need an API key from the Nevo dashboard (starts with `nvo_live_`).

```python
import asyncio, os
from nevo import Nevo

async def main():
    async with Nevo(token=os.environ["NEVO_API_KEY"]) as client:

        @client.on_event()
        async def handle(event):
            if event.type == "email.received":
                await event.reply(text="Got it — on it.")
            elif event.type == "webhook.received":
                print(event.webhook.method, event.webhook.path)

        await client.run()

asyncio.run(main())
```

For scripts that don't want to manage the event loop:

```python
client.run_sync()
```

### Events

Every event exposes the following fields:

| Field          | Type                  |                                        |
| -------------- | --------------------- | -------------------------------------- |
| `id`           | `str`                 | Stable across replays                  |
| `type`         | `str`                 | `webhook.received` or `email.received` |
| `origin`       | `str`                 | `live` or `replay`                     |
| `created_at`   | `datetime`            | Server-side ingest time                |
| `channel`      | `Channel`             | The channel that received the event    |
| `data`         | `dict`                | The raw source payload                 |
| `webhook`      | `WebhookData \| None` | Set when `type == "webhook.received"`  |
| `email`        | `EmailData \| None`   | Set when `type == "email.received"`    |
| `prompt_ready` | `str`                 | Text rendering of the event (useful when the next hop is a model) |

Branch on `event.type`:

```python
@client.on_event()
async def handle(event):
    if event.type == "email.received":
        print(event.email.from_, event.email.subject)
    elif event.type == "webhook.received":
        print(event.webhook.method, event.webhook.path)
```

### Replies

Respond on the same channel the event arrived on. Email replies are threaded into the original conversation automatically.

```python
await event.reply(
    text="Thanks for your message.",
    subject="Re: API limits",      # email-only; defaults to "Re: <original>"
    cc=["ops@acme.com"],            # email-only
)
```

Channels that don't accept replies (webhook, cron) raise `UnsupportedChannelError` client-side — no round-trip.

### Configuration

```python
client = Nevo(
    token="nvo_live_...",
    handler_timeout=30.0,
    reconnect_max_backoff=30.0,
    http_timeout=10.0,
    http_max_retries=3,
    logger=None,
)
```

| Option                  | Default                     | Description                                  |
| ----------------------- | --------------------------- | -------------------------------------------- |
| `token`                 | —                           | API key from the Nevo dashboard              |
| `handler_timeout`       | `30.0`                      | Seconds before a handler is considered stuck |
| `reconnect_max_backoff` | `30.0`                      | Cap on exponential reconnect delay           |
| `http_timeout`          | `10.0`                      | Per-request timeout for HTTP calls           |
| `http_max_retries`      | `3`                         | Retries for transient HTTP failures          |
| `logger`                | `logging.getLogger("nevo")` | Your own `logging.Logger`                    |

### Handler behavior

Handlers must be declared with `async def`. One handler per client — branch on `event.type` inside it.

- Returning cleanly means the event was handled.
- Raising logs the exception and continues the stream.
- Exceeding `handler_timeout` is logged as a timeout and the stream continues.

Events are not re-delivered to the SDK on handler failure. Replay them from the dashboard when you need to.

### Reconnects

The client reconnects automatically with exponential jittered backoff, capped at `reconnect_max_backoff`. A 401 from the server raises `AuthError` and does not retry — fix the key and reconnect.

### Logging

The SDK logs under the `nevo` logger:

```python
import logging
logging.getLogger("nevo").setLevel(logging.DEBUG)
```

## Errors

All exceptions inherit from `nevo.NevoError`. Catch broadly, act specifically.

| Exception                 | Raised when                                       | Retryable |
| ------------------------- | ------------------------------------------------- | --------- |
| `AuthError`               | API key rejected by the server                    | No        |
| `ConnectionError`         | WebSocket cannot connect after retries            | No        |
| `HandlerTimeoutError`     | Handler exceeded `handler_timeout`                | No        |
| `ReplyError`              | Base class for reply failures                     | —         |
| `UnsupportedChannelError` | Channel type doesn't accept replies               | No        |
| `RequestError`            | Client-side validation or server 4xx              | No        |
| `ServerError`             | Server 5xx after retries                          | Yes       |
| `NetworkError`            | Network failure after retries                     | Yes       |

```python
from nevo import Nevo, AuthError, ReplyError

try:
    asyncio.run(client.run())
except AuthError:
    # Fix the key and restart.
    ...

try:
    await event.reply(text="…")
except ReplyError as exc:
    if exc.is_transient:
        # Enqueue for later retry.
        ...
```

## Support

- Issues: [github.com/nevo-sh/sdk-python/issues](https://github.com/nevo-sh/sdk-python/issues)
- Email: [support@nevo.sh](mailto:support@nevo.sh)

## License

MIT
