Metadata-Version: 2.4
Name: doja-sdk
Version: 0.9.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
1. [WhatsApp (Mensajería, Templates, Multimedia y Recepción)](#1-módulo-de-whatsapp)
2. [Email (Texto, HTML y Adjuntos)](#2-módulo-de-email)
3. [Plantillas de WhatsApp (Administración / CRUD)](#3-plantillas-de-whatsapp-administración)
4. [Consulta de Cuotas Restantes 📊](#4-consulta-de-cuotas-restantes)
5. [Módulo Asíncrono (AsyncIO) ⚡](#5-módulo-asíncrono-asyncio)
6. [Manejo de Errores y Excepciones](#6-manejo-de-errores)
7. [Optimización y Desempeño](#7-optimización-y-desempeño)

---

## 1. Módulo de WhatsApp 💬

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

### Configuración Inicial
```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}")
```

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

**Mensaje de 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 botones):**
```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
)
```

**Templates Aprobados por Meta:**
*(Requerido para iniciar conversación fuera de la ventana de atención de 24 hrs)*

*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",
    }
)
```

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

### Descarga 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)** — útil si quieres hacer streaming o descargar tú mismo:
```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 autenticarse. Para que tu CRM pueda mostrar la imagen días o meses después, sube `media["content"]` a tu propio almacenamiento (S3, Cloudflare R2, GCS, disco local) y guarda la URL pública resultante en el CRM.

**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` que ya expiró o no existe → `DojaAPIError` (404).
- Token de WhatsApp inválido → `DojaAPIError` (401).

---

## 2. Módulo de Email 📧

El Módulo de Email está diseñado para ser extremadamente intuitivo. Para autenticarte, **no requieres doble token**. Tu clave de licencia de DoJa funciona automáticamente como la llave para despachar los correos mediante nuestro proveedor subyacente (Resend).

### Configuracion Rapida - Email

```python
from doja_sdk import DojaEmailClient

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}")
```

### Ejemplos de Envío (Email)

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

**Email con 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
)
```

**Email con Archivos Adjuntos (Rutas Locales o URLs):**
*(El SDK procesa archivos locales codificándolos en Base64 o transfiere la URL de forma 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
    ]
)
```

**Funciones Avanzadas (Múltiples destinatarios, CC, BCC y Reply-To):**
```python
email.send(
    to="gerente@empresa.com, asistente@empresa.com",   # Puedes separar 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"]
)
```

---

## 3. Plantillas de WhatsApp (Administración) 🧩

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 Inicial
```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
```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"}
```

### 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")
```

### Versión Asíncrona
```python
import asyncio
from doja_sdk import AsyncDojaTemplateClient

async def main():
    tpl = AsyncDojaTemplateClient(
        doja_token="TU-LICENCIA-DOJA",
        whatsapp_token="EAAB...",
        waba_id="123456789012345"
    )
    plantillas = await tpl.list_templates(limit=20)
    print(plantillas)

asyncio.run(main())
```

---

## 4. Consulta de Cuotas Restantes 📊

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

### Standalone (recomendado para chequeos rápidos)
```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
import asyncio
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")

asyncio.run(main())
```

### Desde un cliente ya instanciado
Cualquier cliente (`DojaClient`, `DojaEmailClient`, `DojaTemplateClient`, etc.) expone los contadores como atributos y un método para refrescarlos:

```python
from doja_sdk import DojaClient

wa = DojaClient(doja_token="...", whatsapp_token="...", phone_id="...")

# Atributos disponibles tras instanciar:
print(wa.remaining_messages, wa.remaining_emails)
print(wa.used_messages, wa.used_emails)
print(wa.current_period_end)

# Refrescar (golpea el core):
quotas = wa.get_remaining_quotas()

# Usar caché de 5 min para evitar latencia:
quotas = wa.get_remaining_quotas(use_cache=True)
```

### 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 justamente que puedas detectarlo leyendo `q["can_send"]` o `q["messages"]["remaining"]`.

---

## 5. Módulo Asíncrono (AsyncIO) ⚡

Ideal para aplicaciones modernas como **FastAPI** o procesos que requieren alta concurrencia. El SDK asíncrono utiliza `httpx` bajo el capó para realizar operaciones no bloqueantes.

### WhatsApp Asíncrono
```python
import asyncio
from doja_sdk import AsyncDojaClient

async def main():
    wa = AsyncDojaClient(
        doja_token="TU-LICENCIA-DOJA",
        whatsapp_token="EAAB...",
        phone_id="123456789"
    )

    # Envío no bloqueante
    await wa.send_text("525512345678", "¡Hola desde Async SDK!")

asyncio.run(main())
```

### 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
    )
```

---

## 6. Manejo de Errores 🛡️

El SDK expone excepciones estandarizadas que te permiten saber exactamente qué falló (si fue un problema de saldo, de licencia o de la API destino). Las puedes importar todas desde `doja_sdk`:

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

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

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

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

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

---

## 7. Optimización y Desempeño 🚀

Esta versión del SDK incluye mejoras críticas:

*   **Caché de Licencia:** La validación de licencia se cachea localmente durante 5 minutos para evitar picos de latencia en cada mensaje enviado.
*   **Logging:** Hemos reemplazado los `print` por el módulo `logging`. Puedes configurar el nivel de detalle:
    ```python
    import logging
    logging.getLogger("doja_sdk").setLevel(logging.DEBUG)
    ```
*   **Consumo Robusto:** Las llamadas de consumo de créditos se ejecutan con manejadores de errores internos para asegurar que un fallo en el servidor de licencias no interrumpa tu proceso principal.

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