Metadata-Version: 2.4
Name: supporthub-sdk
Version: 0.6.0
Summary: Official Python SDK for the SupportHub REST API
License: MIT
Keywords: supporthub,support,helpdesk,api,sdk
Author: SupportHub
Author-email: support@forestsnet.com
Requires-Python: >=3.10,<4.0
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: Programming Language :: Python :: 3.14
Requires-Dist: httpx (>=0.27,<0.29)
Requires-Dist: pydantic (>=2.0,<3.0)
Project-URL: Documentation, https://support.forestsnet.com/docs/api-guide
Project-URL: Homepage, https://support.forestsnet.com
Project-URL: Repository, https://github.com/forestsnet/support-saas
Description-Content-Type: text/markdown

# SupportHub Python SDK

Official Python SDK for the [SupportHub](https://support.forestsnet.com) REST API.

It provides ergonomic synchronous and asynchronous clients, fully typed
[pydantic v2](https://docs.pydantic.dev/) response models, and a
high-level long-polling helper for real-time updates.

- API base URL: `https://api.support.forestsnet.com/api/v1`
- Auth: `Authorization: Bearer sk_...`
- Full API docs: <https://support.forestsnet.com/docs/api-guide>

## Installation

With Poetry:

```bash
poetry add supporthub-sdk
```

With pip:

```bash
pip install supporthub-sdk
```

Requires Python 3.10+.

## Quick start

### Synchronous

```python
from supporthub import Client

with Client(api_key="sk_live_...") as client:
    page = client.tickets.list(status="open", page=1)
    for ticket in page.items:
        print(ticket.id, ticket.subject, ticket.status)

    ticket = client.tickets.create(
        contact={"email": "alice@example.com", "name": "Alice"},
        subject="Cannot log in",
        message="I get a 500 error when logging in.",
        priority="high",
    )

    client.messages.send(ticket_id=ticket.id, content="Thanks, looking into it!")
    client.tickets.close(ticket.id)
```

### Asynchronous

```python
import asyncio
from supporthub import AsyncClient

async def main():
    async with AsyncClient(api_key="sk_live_...") as client:
        page = await client.tickets.list(status="open")
        for t in page.items:
            print(t.id, t.subject)

asyncio.run(main())
```

## Configuration

```python
Client(
    api_key="sk_...",
    base_url="https://api.support.forestsnet.com/api/v1",  # optional
    timeout=30.0,                                           # optional
)
```

You can also inject your own `httpx.Client` / `httpx.AsyncClient` via
`http_client=` if you need custom transports, proxies, retries, etc.

## Resources

All clients expose the same resource attributes:
`client.tickets`, `client.messages`, `client.contacts`,
`client.updates`, `client.webhooks`.

### Tickets

```python
page    = client.tickets.list(status="open", priority="high", page=1, page_size=20)
detail  = client.tickets.get("ticket-uuid", last_messages=50)
ticket  = client.tickets.create(
    contact={"email": "user@example.com"},
    subject="Help",
    message="Initial message",
    priority="normal",
    tags=["billing"],
)
ticket  = client.tickets.update("ticket-uuid", status="resolved")
ticket  = client.tickets.close("ticket-uuid")
ticket  = client.tickets.reopen("ticket-uuid")
```

### Messages

```python
page = client.messages.list(ticket_id="ticket-uuid", page=1, page_size=50)
msg  = client.messages.send(ticket_id="ticket-uuid", content="Hello!", is_internal=False)

# Inbound from an external source (creates/finds contact and ticket automatically):
result = client.messages.create_inbound(
    contact={"email": "user@example.com", "name": "Alice"},
    message="Hi, I have a question",
    session_id="sess-abc",
)
```

### Contacts

```python
contacts = client.contacts.list(search="alice", page=1)
contact  = client.contacts.get("contact-uuid")
contact  = client.contacts.upsert(
    internal_id="user-123",
    full_name="Alice Smith",
    email="alice@example.com",
)
```

### Updates (long polling)

Telegram-style long polling. The server holds the connection open until new
events arrive or `timeout` seconds pass.

```python
offset = 0
while True:
    updates = client.updates.poll(
        offset=offset,
        timeout=25,
        types=["ticket.created", "message.created"],
    )
    for u in updates:
        print(u.type, u.data)
        offset = max(offset, u.id)
```

The async client provides a higher-level streaming helper that tracks the
offset internally and yields events as they arrive:

```python
async with AsyncClient(api_key="sk_...") as client:
    async for update in client.updates.stream(types=["message.created"]):
        print(update.type, update.data)
```

### Webhooks

```python
webhooks = client.webhooks.list()
wh = client.webhooks.create(
    url="https://example.com/hooks/supporthub",
    events=["ticket.created", "message.created"],
    description="Production ingestion",
)
print(wh.secret)  # only returned once, on creation
client.webhooks.delete(wh.id)
```

### Media

Upload, attach to a message, and download files:

```python
# Upload (path, bytes, or file-like)
media = client.media.upload("./photo.jpg")

# Attach to an outbound message
client.messages.send(
    ticket_id=ticket.id,
    content="See attached",
    media_ids=[media.id],
)

# Download the raw bytes
blob = client.media.download(media.id)
with open("out.jpg", "wb") as f:
    f.write(blob)
```

Messages come back with a `media` array of `MediaFile` items:

```python
detail = client.tickets.get(ticket.id)
for m in detail.messages:
    for mf in m.media:
        print(mf.id, mf.url, mf.mime_type, mf.file_type)
```

### Ticket ratings

```python
client.tickets.rate(ticket_id, 5, comment="Отлично!")
```

## Long polling vs webhooks

- **Webhooks** are best for production servers with a public HTTPS endpoint.
  SupportHub will POST events to your URL and sign them with your webhook
  secret.
- **Long polling** (`/updates`) is great for development, prototyping,
  workers behind NAT, or any environment without a public callback URL.
  Use `AsyncClient.updates.stream(...)` for an async iterator that handles
  offset tracking and reconnection for you.

## Error handling

All HTTP errors raise subclasses of `supporthub.APIError`:

| Status | Exception          |
|-------:|--------------------|
| 400    | `ValidationError`  |
| 401    | `AuthError`        |
| 403    | `PermissionError`  |
| 404    | `NotFoundError`    |
| 422    | `ValidationError`  |
| 429    | `RateLimitError`   |
| 5xx    | `ServerError`      |

Each exception exposes:

- `.status_code` — HTTP status
- `.error_code`  — backend `error_code` if present
- `.message`     — human-readable message
- `.detail`      — raw `detail` payload from the API

```python
from supporthub import Client, RateLimitError, AuthError, APIError

try:
    client.tickets.list()
except AuthError:
    print("Bad API key")
except RateLimitError as e:
    print("Slow down, retry after", e.retry_after)
except APIError as e:
    print("API error:", e.status_code, e.message)
```

## Models

All responses are parsed into pydantic v2 models with `extra="allow"` so
unknown future fields will not break your code. Common types:

- `Ticket`, `TicketDetail`, `TicketStatus`, `TicketPriority`
- `Message`, `SenderType`
- `Contact`
- `Update`, `UpdatesResponse`
- `Webhook`
- `PaginatedTickets`, `PaginatedMessages`, `PaginatedContacts`

Use `model.model_dump()` to get a plain dict.

## Development

```bash
cd sdks/python
poetry install
poetry run pytest
poetry run ruff check .
```

## License

MIT

