Metadata-Version: 2.4
Name: doja-sdk
Version: 0.11.0
Summary: Official Python SDK for DoJa Chatbots and WhatsApp Business Integration
Author-email: DoJa Consulting <contact@dojaconsulting.cloud>
Project-URL: Homepage, https://dojaconsulting.cloud
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.7
Description-Content-Type: text/markdown
Requires-Dist: requests
Requires-Dist: httpx

# DoJa Python SDK 🚀

El **DoJa SDK** es la biblioteca oficial de Python para conectar, automatizar y escalar envíos masivos o transaccionales a través de **WhatsApp Cloud API (Meta)** y el **Gateway de Emails DoJa**.

Con DoJa SDK, te olvidas de integraciones complejas. Usa **la misma licencia** para disparar notificaciones en ambos canales mediante un código elegante, rápido y sin dependencias pesadas.

---

## 💻 Instalación

Solo requieres Python 3.7+ y `pip`. El SDK **no** empuja dependencias pesadas ni requiere que instales las librerías oficiales de Meta o proveedores externos de Email:

```bash
pip install doja-sdk
```

---

## 📚 Índice

El SDK está organizado por **canal**. Cada canal es independiente: usa solo el que necesites.

**💬 WhatsApp**
1. [Configuración del cliente](#11-configuración-del-cliente)
2. [Envío de mensajes](#12-envío-de-mensajes)
3. [Envío de plantillas aprobadas](#13-envío-de-plantillas-aprobadas)
4. [Recepción de media entrante (webhooks)](#14-recepción-de-media-entrante-webhooks)
5. [Administración de plantillas (CRUD)](#15-administración-de-plantillas-crud)
6. [Crear plantillas con multimedia 🆕](#16-crear-plantillas-con-multimedia-)
7. [WhatsApp asíncrono](#17-whatsapp-asíncrono)

**📧 Email**
8. [Configuración y envío](#21-configuración-y-envío)
9. [Email asíncrono](#22-email-asíncrono)

**🧰 Común a ambos canales**
10. [Consulta de cuotas restantes](#31-consulta-de-cuotas-restantes)
11. [Manejo de errores](#32-manejo-de-errores)
12. [Optimización y desempeño](#33-optimización-y-desempeño)

---
---

# 💬 WhatsApp

Todo lo relacionado con WhatsApp Cloud API vive aquí: enviar mensajes, plantillas, recibir media y administrar templates.

## 1.1 Configuración del cliente

Para interactuar con WhatsApp necesitas tu Token de Licencia DoJa, tu Token de Meta y tu Phone ID.

```python
from doja_sdk import DojaClient, DojaAuthError

try:
    wa = DojaClient(
        doja_token="TU-LICENCIA-DOJA",   # Token de tu suscripción en DoJa
        whatsapp_token="EAAB12345...",    # Token Permanente/Temporal de Meta
        phone_id="100747123456"           # Identificador de tu número en Meta
    )
    print("¡Conexión a WhatsApp exitosa!")
except DojaAuthError as e:
    print(f"Error de licencia: {e}")
```

## 1.2 Envío de mensajes

*(Nota: El destinatario debe incluir código de país sin el signo `+`. Ej: `"525512345678"`)*

**Texto simple:**
```python
wa.send_text("525512345678", "¡Hola! Hemos recibido tu pago exitosamente.")
```

**Documentos (PDF, facturas, Excel):**
```python
wa.send_document(
    to="525512345678",
    url="https://ejemplo.com/factura.pdf",
    caption="Aquí tienes tu factura del mes 📝",
    filename="Factura_Octubre.pdf"   # Opcional: nombre con el que se descarga
)
```

**Imágenes y promocionales:**
```python
wa.send_image(
    to="525512345678",
    url="https://ejemplo.com/promo.jpg",
    caption="¡Aprovecha nuestro descuento de temporada!"
)
```

**Ubicación compartida (abre interactivo en Google Maps/Waze):**
```python
wa.send_location(
    to="525512345678",
    latitude=19.432608,
    longitude=-99.133209,
    name="Sucursal Centro",
    address="Centro Histórico, CDMX, México"
)
```

**Botones interactivos (máximo 3):**
```python
botones = [
    {"id": "soporte", "title": "Hablar con Soporte"},
    {"id": "ventas", "title": "Cotizar Servicios"}
]

wa.send_interactive_button(
    to="525512345678",
    body_text="¿En qué área podemos apoyarte hoy?",
    buttons_list=botones
)
```

**Listas desplegables (ideal para menús largos):**
```python
secciones = [
    {
        "title": "Áreas de Atención",
        "rows": [
            {"id": "1", "title": "Soporte", "description": "Fallas técnicas"},
            {"id": "2", "title": "Ventas", "description": "Nuevas suscripciones"}
        ]
    }
]

wa.send_interactive_list(
    to="525512345678",
    body_text="Por favor selecciona el área deseada:",
    button_text="Ver Opciones",
    sections=secciones
)
```

## 1.3 Envío de plantillas aprobadas

*(Requerido para iniciar conversación fuera de la ventana de atención de 24 hrs. La plantilla debe estar `APPROVED` en Meta.)*

**Variables POSICIONALES (`{{1}}`, `{{2}}`, ...):**
```python
wa.send_template(
    to="525512345678",
    template_name="confirmacion_pedido",   # Nombre idéntico al de Meta
    language_code="es",
    body_variables=["Juan", "#123456", "Mañana a las 15:00 hrs"]
)
```

**Variables NOMBRADAS (`{{nombre_cliente}}`, `{{folio}}`, ...):**
```python
wa.send_template(
    to="525512345678",
    template_name="confirmacion_pedido",
    language_code="es",
    body_variables_named={
        "nombre_cliente": "Juan",
        "folio": "#123456",
        "fecha_entrega": "Mañana a las 15:00 hrs",
    }
)
```

**Plantilla con header de imagen** (la imagen real al momento de enviar):
```python
wa.send_template(
    to="525512345678",
    template_name="promo_junio",
    language_code="es",
    header_image_url="https://tu-cdn.com/promo.jpg",
    body_variables=["Juan"]
)
```

> [!NOTE]
> `body_variables` y `body_variables_named` son **mutuamente excluyentes**. Una plantilla de WhatsApp usa o placeholders posicionales (`{{1}}`) o nombrados (`{{nombre}}`), nunca ambos en el mismo body. Si pasas los dos, el SDK lanza `DojaValidationError`.

## 1.4 Recepción de media entrante (webhooks)

Cuando un cliente final te envía una **imagen, documento, audio, video o sticker** desde WhatsApp, el webhook de Meta **no entrega el archivo**: solo entrega un `media_id`. Si guardas ese `media_id` directo en tu CRM, los archivos "no se visualizan" — porque no son una URL pública, son solo un identificador.

`download_media` resuelve ese problema en una sola llamada:

1. Convierte el `media_id` en la URL temporal que expone Meta (vida ~5 min, requiere bearer token).
2. Descarga el binario con el mismo token.
3. Devuelve URL + metadata (`mime_type`, `sha256`, `file_size`) + `content` (bytes), listo para subir a tu S3/CDN o reenviar al CRM.

> ✅ **Esta llamada NO consume crédito de tu licencia DoJa.** Es media entrante, no es un mensaje enviado por tu cuenta.

**Ejemplo: imagen entrante recibida en tu webhook**
```python
# En tu handler del webhook entrante de WhatsApp:
mensaje = payload["entry"][0]["changes"][0]["value"]["messages"][0]

if mensaje.get("type") == "image":
    media_id = mensaje["image"]["id"]

    media = wa.download_media(media_id)

    print(media["mime_type"])   # "image/jpeg"
    print(media["file_size"])   # 248531
    print(media["sha256"])      # hash que Meta entrega

    # Guarda el binario en TU almacenamiento (S3, GCS, R2, disco)
    with open(f"/uploads/{media_id}.jpg", "wb") as f:
        f.write(media["content"])
```

**Mismo método para documentos, audio, video y stickers** — solo cambia la clave del payload (`document`, `audio`, `video`, `sticker`):
```python
if mensaje.get("type") == "document":
    media = wa.download_media(mensaje["document"]["id"])
    nombre_original = mensaje["document"].get("filename", "archivo.bin")
    # media["mime_type"] -> "application/pdf", "application/vnd.openxmlformats..."

if mensaje.get("type") == "audio":
    media = wa.download_media(mensaje["audio"]["id"])
    # media["mime_type"] -> "audio/ogg" (notas de voz), "audio/mpeg", ...
```

**Solo URL temporal (sin bajar el binario):**
```python
media = wa.download_media(media_id, include_bytes=False)
# media["url"]: URL de Meta. Descárgala con el header
# Authorization: Bearer <whatsapp_token>
```

> ⚠️ **No guardes `media["url"]` directo en tu CRM.** Es una URL temporal de Meta que **expira en ~5 minutos** y requiere el token de WhatsApp. Para que tu CRM muestre la imagen días o meses después, sube `media["content"]` a tu propio almacenamiento (S3, R2, GCS) y guarda la URL pública resultante.

**Forma del dict retornado:**
```python
{
    "id": "1234567890",
    "url": "https://lookaside.fbsbx.com/whatsapp_business/...",
    "mime_type": "image/jpeg",
    "sha256": "a3b1c2d4...",
    "file_size": 248531,
    "messaging_product": "whatsapp",
    "content": b"\xff\xd8\xff\xe0..."   # bytes — solo si include_bytes=True
}
```

**Errores típicos:**
- `media_id` vacío → `DojaValidationError`.
- `media_id` expirado o inexistente → `DojaAPIError` (404).
- Token de WhatsApp inválido → `DojaAPIError` (401).

## 1.5 Administración de plantillas (CRUD)

Si necesitas **registrar, listar, actualizar o eliminar plantillas** (templates) en tu WhatsApp Business Account directamente desde código, usa `DojaTemplateClient`. A diferencia del envío, aquí necesitas tu **WABA ID** (WhatsApp Business Account ID), no el `phone_id`. Estas operaciones **no consumen créditos** de tu licencia DoJa.

> ⚠️ Las plantillas creadas entran en estado `PENDING` hasta que Meta las apruebe. Solo cuando estén `APPROVED` podrás enviarlas con `wa.send_template(...)`.

### Configuración
```python
from doja_sdk import DojaTemplateClient

tpl = DojaTemplateClient(
    doja_token="TU-LICENCIA-DOJA",
    whatsapp_token="EAAB12345...",     # Mismo token de Meta
    waba_id="123456789012345"          # ID de la WhatsApp Business Account
)
```

### Listar plantillas
```python
res = tpl.list_templates(limit=50)
for t in res.get("data", []):
    print(t["name"], "—", t["status"], "—", t["category"])

# Filtrar por nombre o estado
tpl.list_templates(name="confirmacion_pedido")
tpl.list_templates(status="APPROVED")
```

### Crear plantilla (solo texto/botones)
```python
tpl.create_template(
    name="confirmacion_pedido",
    language="es_MX",
    category="UTILITY",   # AUTHENTICATION | MARKETING | UTILITY
    components=[
        {"type": "HEADER", "format": "TEXT", "text": "Pedido confirmado"},
        {
            "type": "BODY",
            "text": "Hola {{1}}, tu pedido {{2}} está listo para envío el {{3}}.",
            "example": {"body_text": [["Juan", "#12345", "mañana"]]}
        },
        {"type": "FOOTER", "text": "Gracias por tu compra"},
        {
            "type": "BUTTONS",
            "buttons": [
                {"type": "QUICK_REPLY", "text": "Ver pedido"},
                {"type": "QUICK_REPLY", "text": "Hablar con soporte"}
            ]
        }
    ]
)
# → {"id": "1234567890", "status": "PENDING", "category": "UTILITY"}
```

> 📌 ¿Quieres una plantilla con **header de imagen/video/documento**? No basta con una URL: Meta exige un `header_handle`. Mira la sección [1.6](#16-crear-plantillas-con-multimedia-).

### Actualizar plantilla
```python
tpl.update_template(
    template_id="1234567890",
    components=[
        {"type": "BODY",
         "text": "Hola {{1}}, tu pedido {{2}} fue actualizado.",
         "example": {"body_text": [["Juan", "#12345"]]}}
    ]
)
```

### Eliminar plantilla
```python
# Por nombre (elimina todas las versiones / idiomas con ese nombre)
tpl.delete_template(name="confirmacion_pedido")

# Por versión específica
tpl.delete_template(name="confirmacion_pedido", template_id="1234567890")
```

## 1.6 Crear plantillas con multimedia 🆕

> Disponible desde **v0.10.0**.

Para crear una plantilla con un **header de imagen, video o documento**, Meta **no** acepta una URL ni un `media_id`. Exige un `header_handle`: un identificador especial que se obtiene subiendo el archivo a la **Resumable Upload API** de Meta. Esto es lo mismo que hace la UI de Meta por detrás cuando arrastras una imagen al crear una plantilla.

El SDK lo resuelve con `upload_media_handle`, que hace la subida en dos pasos y te devuelve el handle listo para usar.

> 🔑 **Requiere `app_id`** (el identificador público de tu App de Meta) en el constructor de `DojaTemplateClient`. No es un secreto: lo ves en *App settings* del dashboard de Meta for Developers.

```python
from doja_sdk import DojaTemplateClient

tpl = DojaTemplateClient(
    doja_token="TU-LICENCIA-DOJA",
    whatsapp_token="EAAB12345...",
    waba_id="123456789012345",
    app_id="27063109049979489",   # 🆕 requerido solo para subir media
)

# 1) Sube el archivo y obtén el header_handle
with open("promo.jpg", "rb") as f:
    file_bytes = f.read()

handle = tpl.upload_media_handle(
    file_bytes=file_bytes,
    file_name="promo.jpg",
    mime_type="image/jpeg",
)

# 2) Crea la plantilla usando el handle en example.header_handle
tpl.create_template(
    name="promo_junio",
    language="es_MX",
    category="MARKETING",
    components=[
        {
            "type": "HEADER",
            "format": "IMAGE",                     # IMAGE | VIDEO | DOCUMENT
            "example": {"header_handle": [handle]}  # 👈 el handle del paso 1
        },
        {
            "type": "BODY",
            "text": "Hola {{1}}, tenemos una oferta para ti 🎉",
            "example": {"body_text": [["Juan"]]}
        }
    ]
)
# → {"id": "...", "status": "PENDING"}
```

**Firma:**
```python
handle: str = tpl.upload_media_handle(file_bytes: bytes, file_name: str, mime_type: str)
```

**Errores típicos:**
- Cliente sin `app_id` → `DojaValidationError`.
- `file_bytes` vacío → `DojaValidationError`.
- Meta rechaza la subida (token sin permisos, tipo no soportado) → `DojaAPIError`.

## 1.7 WhatsApp asíncrono

Ideal para apps modernas (**FastAPI**) o procesos con alta concurrencia. El SDK asíncrono usa `httpx` bajo el capó.

```python
import asyncio
from doja_sdk import AsyncDojaClient

async def main():
    wa = AsyncDojaClient(
        doja_token="TU-LICENCIA-DOJA",
        whatsapp_token="EAAB...",
        phone_id="123456789"
    )
    await wa.send_text("525512345678", "¡Hola desde Async SDK!")

asyncio.run(main())
```

Las plantillas también tienen versión async — misma API, incluido `upload_media_handle`:
```python
import asyncio
from doja_sdk import AsyncDojaTemplateClient

async def main():
    tpl = AsyncDojaTemplateClient(
        doja_token="TU-LICENCIA-DOJA",
        whatsapp_token="EAAB...",
        waba_id="123456789012345",
        app_id="27063109049979489",   # solo si vas a subir media
    )
    plantillas = await tpl.list_templates(limit=20)
    print(plantillas)

asyncio.run(main())
```

---
---

# 📧 Email

El Módulo de Email está diseñado para ser extremadamente intuitivo. **No requiere doble token**: tu clave de licencia de DoJa funciona como la llave para despachar los correos mediante el proveedor subyacente (Resend).

## 2.1 Configuración y envío

```python
from doja_sdk import DojaEmailClient, DojaAuthError

try:
    email = DojaEmailClient(
        doja_token="TU-LICENCIA-DOJA",           # La licencia que DoJa valida
        external_id="LA_LLAVE_DE_SERVICIO",      # ID proporcionado por el integrador
        from_address="no-reply@tuempresa.com"    # Remitente verificado
    )
    print("¡Conexión de Email exitosa!")
except DojaAuthError as e:
    print(f"Error de licencia o permisos: {e}")
```

**Texto plano:**
```python
email.send(
    to="cliente@empresa.com",
    subject="Confirmación de Registro",
    body="Gracias por registrarte en nuestra plataforma."
)
```

**HTML enriquecido:**
```python
email.send(
    to="cliente@empresa.com",
    subject="Bienvenido a tu suscripción Pro",
    body="<h1>¡Hola Juan!</h1><p>Tu cuenta ya está activa. <a href='https://app.doja.com'>Ingresa aquí</a>.</p>",
    html=True
)
```

**Archivos adjuntos (rutas locales o URLs):**
*(El SDK codifica archivos locales en Base64 o transfiere la URL directa al Gateway.)*
```python
email.send(
    to="cliente@empresa.com",
    subject="Tu Factura de Marzo",
    body="Adjuntamos tu factura digitalizada correspondiente a este mes.",
    attachments=[
        "/ruta/absoluta/factura_marzo.pdf",          # Archivo local
        "https://ejemplo.com/cotizacion_final.xlsx"  # Archivo por URL
    ]
)
```

**Múltiples destinatarios, CC, BCC y Reply-To:**
```python
email.send(
    to="gerente@empresa.com, asistente@empresa.com",   # separa por comas
    subject="Reporte Financiero Semanal",
    body="<h2>Reporte Generado</h2><p>Aquí tienes el archivo de esta semana.</p>",
    html=True,
    cc=["director@empresa.com"],
    bcc=["auditoria@empresa.com"],
    reply_to="soporte@empresa.com",
    attachments=["/ruta/reporte.xlsx", "/ruta/resumen.pdf"]
)
```

## 2.2 Email asíncrono

```python
from doja_sdk import AsyncDojaEmailClient

async def send_welcome():
    email = AsyncDojaEmailClient(
        doja_token="TU-LICENCIA-DOJA",
        external_id="LLAVE_RESEND",
        from_address="hola@tuempresa.com"
    )

    await email.send(
        to="cliente@ejemplo.com",
        subject="Bienvenido",
        body="<h1>Registro Exitoso</h1>",
        html=True
    )
```

---
---

# 🧰 Común a ambos canales

## 3.1 Consulta de cuotas restantes

¿Quieres saber **cuántos mensajes de WhatsApp y correos te quedan**? Usa `get_remaining_quotas`. Solo necesitas tu `doja_token` — no requiere instanciar un cliente completo y **no consume crédito**.

```python
from doja_sdk import get_remaining_quotas

q = get_remaining_quotas("TU-LICENCIA-DOJA")

print(f"Mensajes restantes: {q['messages']['remaining']}/{q['messages']['included']}")
print(f"Correos restantes:  {q['emails']['remaining']}/{q['emails']['included']}")
print(f"Periodo activo:     {q['period']['start']} → {q['period']['end']}")
print(f"Puede enviar:       {q['can_send']}")
```

**Versión asíncrona:**
```python
from doja_sdk import get_remaining_quotas_async

async def main():
    q = await get_remaining_quotas_async("TU-LICENCIA-DOJA")
    print(q["messages"]["remaining"], "mensajes")
    print(q["emails"]["remaining"], "correos")
```

**Desde un cliente ya instanciado** — cualquier cliente (`DojaClient`, `DojaEmailClient`, `DojaTemplateClient`...) expone los contadores como atributos:
```python
wa = DojaClient(doja_token="...", whatsapp_token="...", phone_id="...")

print(wa.remaining_messages, wa.remaining_emails)
print(wa.used_messages, wa.used_emails)
print(wa.current_period_end)

quotas = wa.get_remaining_quotas()                 # refresca (golpea el core)
quotas = wa.get_remaining_quotas(use_cache=True)   # caché de 5 min
```

**Forma del dict retornado:**
```python
{
    "valid": True,
    "can_send": True,
    "account_id": "cm_demo_account",
    "account_name": "DoJa Workspace",
    "plan_tier": "ENTERPRISE",
    "subscription_status": "ACTIVE",
    "messages": {"included": 500000, "used": 120, "remaining": 499880},
    "emails":   {"included":  50000, "used":  24, "remaining":  49976},
    "period":   {"start": "2026-05-26T10:00:00.000Z",
                 "end":   "2026-06-26T10:00:00.000Z"},
    "channels": {"allowed": ["whatsapp", "email"],
                 "whatsapp_active": True,
                 "email_active": True}
}
```

> ℹ️ Si el token es inválido o no existe, se lanza `DojaAuthError`. **No** se lanza `DojaQuotaExceeded` aunque la cuota esté agotada — la idea es que lo detectes leyendo `q["can_send"]` o `q["messages"]["remaining"]`.

## 3.2 Manejo de errores

El SDK expone excepciones estandarizadas que te permiten saber exactamente qué falló. Las importas todas desde `doja_sdk`:

```python
from doja_sdk import (
    DojaClient,
    DojaAuthError,
    DojaQuotaExceeded,
    DojaAPIError,
    DojaValidationError,
)

try:
    client = DojaClient(...)
    client.send_text(...)

except DojaAuthError as e:
    # 404 No encontrado, 422 Inválido, 409 Suspendido, o canal no contratado
    print(f"Problema con la Licencia: {e}")

except DojaQuotaExceeded as e:
    # 409 La licencia es válida, pero se acabó la bolsa de mensajes
    print(f"Saldo Agotado: {e}")

except DojaValidationError as e:
    # Parámetros inválidos antes de salir a la red (ej. body_variables + named)
    print(f"Datos inválidos: {e}")

except DojaAPIError as e:
    # Meta o Resend rechazan la petición (plantilla mal armada, teléfono inválido, adjunto pesado)
    print(f"Problema con el Proveedor Destino: {e}")
```

## 3.3 Optimización y desempeño

*   **Caché de licencia:** la validación se cachea localmente 5 minutos para evitar picos de latencia en cada envío.
*   **Logging:** el SDK usa el módulo `logging` (no `print`). Ajusta el nivel:
    ```python
    import logging
    logging.getLogger("doja_sdk").setLevel(logging.DEBUG)
    ```
*   **Consumo robusto:** las llamadas de consumo de créditos tienen manejadores de error internos para que un fallo del servidor de licencias no interrumpa tu proceso principal.

---

**¿Dudas adicionales o soporte técnico?**
Comunícate directamente a: contact@dojaconsulting.cloud
