Metadata-Version: 2.5
Name: dotly
Version: 0.1.1
Summary: SDK oficial de Python para la API de Dotly: WhatsApp, CRM, catálogo, webhooks y Dot AI.
Project-URL: Homepage, https://www.dotly.com.py
Project-URL: Documentation, https://www.dotly.com.py/api/v1/docs
Project-URL: Changelog, https://www.dotly.com.py/api/v1/docs#sdk-python
Author: Joel Ortiz
License-Expression: MIT
License-File: LICENSE
Keywords: api,crm,dot ai,dotly,erp,sdk,webhooks,whatsapp
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
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 :: Communications :: Chat
Classifier: Topic :: Office/Business
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx<1,>=0.25
Provides-Extra: dev
Requires-Dist: build>=1; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: twine>=5; extra == 'dev'
Description-Content-Type: text/markdown

# dotly — SDK oficial de Python para Dotly

Conectá tu sistema con [Dotly](https://www.dotly.com.py): contactos y embudo, conversaciones y mensajes de
WhatsApp, plantillas, catálogo e inventario, webhooks firmados y Dot AI.

```bash
pip install dotly
```

Python 3.9 o superior. Única dependencia: [httpx](https://www.python-httpx.org/).

## Primeros pasos

1. En Dotly, entrá a **API & SDK → Aplicaciones**, creá una aplicación y una clave con los permisos que
   necesitás. La clave (`dtly_live_...`) se muestra una sola vez.
2. Guardala en una variable de entorno, nunca en el código:

```bash
export DOTLY_API_KEY="dtly_live_..."
```

```python
from dotly import Dotly

client = Dotly()  # lee DOTLY_API_KEY

contact = client.contacts.create(phone="595981123456", name="Juan Pérez", tags=["vip"])
client.contacts.update(contact["id"], stage="propuesta", deal_value=2_500_000)

for conversation in client.conversations.iter(status="open"):
    print(conversation["contact"]["name"], conversation["unread_count"])

client.messages.send(to="595981123456", text="¡Hola Juan! Te escribimos de Acme.")
```

Cada clave actúa en nombre de un integrante de tu equipo: puede, como mucho, lo que esa persona puede en
Dotly y ve solo lo que ella ve. Los cambios quedan en la auditoría a su nombre, marcados como hechos por la API.

## Cliente asíncrono

```python
import asyncio
from dotly import AsyncDotly

async def main():
    async with AsyncDotly() as client:
        page = await client.contacts.list(limit=50)
        async for message in client.conversations.iter_messages(conversation_id):
            print(message["text"])

asyncio.run(main())
```

`Dotly` y `AsyncDotly` tienen los mismos recursos y métodos.

## Recursos

| Recurso | Métodos | Permiso de la clave |
|---|---|---|
| `client.contacts` | `list`, `iter`, `create`, `get`, `update`, `delete`, `conversations` | `contacts:read`, `contacts:write`, `contacts:delete` |
| `client.tags` | `list`, `create` (con color) | `contacts:read`, `contacts:write` |
| `client.conversations` | `list`, `iter`, `get`, `update`, `set_labels`, `messages`, `iter_messages`, `send_message` | `conversations:read`, `conversations:write`, `contacts:write`, `messages:send` |
| `client.messages` | `send`, `get`, `edit`, `delete` (para todos), `react` | `messages:send`, `conversations:read` |
| `client.groups` | `preview_invite`, `join`, `import_all`, `reset` | `conversations:write` |
| `client.templates` | `list`, `get`, `flow` (menús y submenús), `render` | `templates:read` |
| `client.members`, `client.teams`, `client.whatsapp_accounts` | `list` | `team:read` |
| `client.scheduled_messages` | `list`, `create`, `cancel` | `conversations:read`, `messages:send` |
| `client.automations` | `list`, `runs` | `automations:read` |
| `client.stats` | `get` | `analytics:read` |
| `client.account` | `get` | `account:read` |
| `client.events` | `list`, `iter` | `events:read` |
| `client.webhooks` | `list`, `create`, `get`, `update`, `delete`, `test`, `rotate_secret` | `webhooks:manage` |
| `client.ai` | `suggest_reply`, `summarize`, `rewrite`, `translate`, `usage` | `ai:use` |
| `client.integration` (= `client.catalog`) | ver abajo | `contacts:write`, `catalog:read`, `catalog:write` |
| `client.embed_sessions` | `create`, `list`, `get`, `revoke` | `embed:sessions` |

Las listas devuelven una `Page` (`data`, `has_more`, `next_cursor`); `iter(...)` recorre todas las páginas.

## Conector de integración (CRM, ERP, tienda)

Tu sistema manda sus datos identificados por **su** id (`external_id`). Repetir un upsert es seguro.

```python
client.integration.contacts.upsert("crm-1042", {
    "name": "Juan Pérez",
    "phone": "595981123456",
    "company": "Acme",
    "source_updated_at": "2026-10-19T14:03:00Z",   # descarta cambios más viejos que lleguen tarde
})

client.integration.items.upsert("SKU-1042", {"name": "Zapatilla Runner", "price": 450000, "track_stock": True, "stock": 25})
client.integration.inventory.set("SKU-1042", 24)

results = client.integration.contacts.batch([
    {"external_id": "crm-1", "data": {"phone": "595981000001", "name": "Ana"}},
    {"external_id": "crm-2", "data": {"phone": "595981000002", "name": "Luis"}},
])
failed = [r for r in results["data"] if "error" in r]   # las demás se aplicaron igual
```

- Un contacto que ya escribió por WhatsApp se **vincula por teléfono**, sin duplicarlo.
- Lo que no enviás queda como está en Dotly (la etapa o las etiquetas que cambió tu equipo no se pisan).
- Sincronización completa del catálogo: `sync_runs.start()`, upserts con `run_id=...` y `sync_runs.complete(run_id, missing="deactivate")`.
  Nunca toca contactos.

## Dotly dentro de tu sistema (embebido)

```python
session = client.embed_sessions.create(origin="https://crm.tuempresa.com", modules=["inbox", "contacts"],
                                       member_email="ana@tuempresa.com", theme={"accent": "#ff6600", "radius": 8})
# session["url"]: una sola vez y vence en 2 minutos. Mostrala en un <iframe> o con dotly-sdk/embed (navegador).
```

El dominio y los módulos se autorizan en Dotly (API & SDK). Dotly se abre con los permisos de esa persona
recortados a los módulos pedidos, y solo se deja mostrar en ese dominio.

## Webhooks

```python
from dotly.webhooks import construct_event, WebhookVerificationError

event = construct_event(raw_body, request.headers.get("Dotly-Signature"), secret)
```

- Verificá siempre con el **cuerpo crudo** (bytes), antes de parsear el JSON.
- Se rechazan firmas inválidas y marcas de tiempo de más de 5 minutos (repeticiones).
- Las entregas son "al menos una vez": guardá `event["id"]` y descartá los repetidos.
- Ejemplos completos con Flask y FastAPI en `examples/`.

## Dot AI

```python
suggestion = client.ai.suggest_reply(conversation_id)
print(suggestion["data"]["text"])
```

Dotly verifica todo en el servidor antes de gastar: suscripción, plan, créditos (aparta el costo máximo y
cobra el real) y los límites de la persona. Si algo falta, recibís una excepción específica
(`InsufficientCreditsError`, `SubscriptionInactiveError`, `AILimitReachedError`, `AIUnavailableError`...).
El SDK no calcula ni guarda saldos.

## Errores y reintentos

Todas las excepciones heredan de `dotly.DotlyError` y traen `status`, `code`, `message`, `details` y `request_id`
(dáselo a soporte si necesitás ayuda).

El SDK reintenta solo lo que es seguro: lecturas y escrituras con `Idempotency-Key`. Cada escritura lleva una
clave única (la misma en sus reintentos), así un reintento nunca duplica un envío ni cobra dos veces Dot AI.
Reintenta errores de red, timeouts, 429 (respetando `Retry-After`) y 5xx. Podés pasar tu propia clave con
`idempotency_key=...` (por ejemplo, el id del pedido en tu sistema).

## Configuración

```python
Dotly(
    api_key="dtly_live_...",      # o DOTLY_API_KEY
    timeout=30,                   # segundos
    max_retries=2,
    app_info="mi-crm/2.3",        # se suma al User-Agent (diagnóstico)
)
```

La URL base es la de Dotly. Una URL propia tiene que ser https y, si no es de dotly.com.py, hay que permitirla
con `allow_custom_base_url=True`: así una configuración manipulada no envía tu clave a otro servidor.

## Versiones

- El SDK sigue versionado semántico. `0.x`: la API v1 está estable; el SDK todavía puede ajustar nombres
  de métodos en versiones menores (se avisa en el CHANGELOG).
- `dotly.API_VERSION` dice para qué versión de la API está hecho (`"1"`). La API suma campos sin cambiar de
  versión: leé lo opcional con `.get(...)`.

Licencia MIT.
