Metadata-Version: 2.4
Name: django-anysms
Version: 0.1.0
Summary: Provider-independent SMS and WhatsApp messaging for Django
Requires-Python: <3.14,>=3.10
Requires-Dist: django<6.1,>=5.2
Provides-Extra: test
Requires-Dist: build>=1.2; extra == 'test'
Requires-Dist: mypy>=1.17; extra == 'test'
Requires-Dist: pytest-django>=4.11; extra == 'test'
Requires-Dist: pytest>=8.4; extra == 'test'
Requires-Dist: pyyaml<7,>=6.0; extra == 'test'
Requires-Dist: ruff>=0.12; extra == 'test'
Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'test'
Requires-Dist: twine<7,>=6; extra == 'test'
Provides-Extra: twilio
Requires-Dist: twilio<10,>=9.11; extra == 'twilio'
Description-Content-Type: text/markdown

# django-anysms

Envio de SMS e WhatsApp com uma API Django consistente e provedores
intercambiáveis. A versão 0.1 inclui Twilio, mensagens síncronas e parsing
validado de callbacks de entrega.

## Instalação

```bash
python -m pip install django-anysms
python -m pip install 'django-anysms[twilio]'
```

A v0.1 suporta Python 3.10–3.13, Django 5.2 LTS e Django 6.0. O SDK da
Twilio é opcional e não é importado pelo core.

## Configuração

Configure aliases em suas settings Django:

```python
import os

ANYSMS = {
    "DEFAULT": "twilio",
    "PROVIDERS": {
        "twilio": {
            "CLASS": "django_anysms.providers.twilio.TwilioProvider",
            "OPTIONS": {
                "account_sid": os.environ["TWILIO_ACCOUNT_SID"],
                "auth_token": os.environ["TWILIO_AUTH_TOKEN"],
                "from_": "+5511999999999",
            },
        },
    },
}
```

O pacote não precisa ser adicionado a `INSTALLED_APPS`.

## SMS

```python
from django_anysms import SMSMessage

result = SMSMessage(
    to="+5511888888888",
    body="Seu código é 123456",
    status_callback="https://example.com/webhooks/twilio/status/",
).send()

print(result.message_id, result.status)
```

Cada mensagem possui exatamente um destinatário em formato E.164. Números
nacionais não são corrigidos ou completados automaticamente.

Um recurso específico pode ser informado sem contaminar a API comum:

```python
message = SMSMessage(
    to="+5511888888888",
    body="Olá",
    provider_options={
        "twilio": {"messaging_service_sid": "MG..."},
    },
)
message.send(using="twilio")
```

## WhatsApp

Texto livre dentro de uma conversa permitida pelo WhatsApp:

```python
from django_anysms import WhatsAppMessage

WhatsAppMessage(
    to="+5511888888888",
    body="Seu pedido saiu para entrega.",
).send()
```

Mensagem proativa com um Content Template aprovado:

```python
WhatsAppMessage(
    to="+5511888888888",
    content_sid="HX0123456789abcdef0123456789abcdef",
    variables={"1": "Rafael", "2": "15/08/2026"},
).send()
```

`body` e `content_sid` são mutuamente exclusivos. O provider adiciona o
prefixo `whatsapp:`; a API pública recebe somente E.164.

## Provider explícito

Settings são opcionais quando a aplicação precisa de credenciais dinâmicas:

```python
from django_anysms import SMSMessage
from django_anysms.providers.twilio import TwilioProvider

provider = TwilioProvider(
    account_sid="AC...",
    auth_token="...",
    from_="+5511999999999",
)

result = SMSMessage(to="+5511888888888", body="Olá").send(provider=provider)
```

A prioridade é: instância em `provider=`, alias em `using=` e, por último,
`ANYSMS["DEFAULT"]`. `provider` e `using` não podem ser usados juntos.

## Erros

Falhas esperadas herdam de `AnySMSError`. Por padrão elas são lançadas. Para
um fluxo compatível com `fail_silently`, use:

```python
result = message.send(fail_silently=True)
if not result.accepted:
    report(result.error)
```

`accepted=True` significa que o provedor aceitou a chamada, não que a
mensagem chegou ao aparelho. Não há retry automático para evitar duplicatas
após falhas ambíguas.

A hierarquia pública inclui `ConfigurationError`, `MessageValidationError`,
`ProviderNotInstalledError`, `ProviderError`, `InvalidWebhookSignature` e
`InvalidWebhookPayload`, todos em `django_anysms.exceptions`.

## Callback de entrega

A aplicação controla sua própria URL e persistência. Exemplo de view:

```python
from django.http import HttpRequest, HttpResponse
from django.views.decorators.csrf import csrf_exempt

from django_anysms.exceptions import InvalidWebhookPayload, InvalidWebhookSignature
from django_anysms.registry import get_provider


@csrf_exempt
def twilio_status(request: HttpRequest) -> HttpResponse:
    provider = get_provider("twilio")
    try:
        event = provider.parse_webhook(
            public_url=request.build_absolute_uri(),
            form=request.POST,
            signature=request.headers.get("X-Twilio-Signature"),
        )
    except InvalidWebhookSignature:
        return HttpResponse(status=403)
    except InvalidWebhookPayload:
        return HttpResponse(status=400)

    consume_delivery_event(event)
    return HttpResponse(status=204)
```

Em ambientes com proxy, `public_url` precisa ser exatamente a URL pública
assinada pela Twilio. A biblioteca valida a assinatura antes de projetar o
payload.

`DeliveryEvent.raw` preserva o formulário decodificado completo, inclusive
campos desconhecidos e valores repetidos. Tanto ele quanto `SendResult.raw`
podem conter telefones e outros dados pessoais; não registre ou persista esses
mappings sem uma política adequada.

Callbacks podem chegar fora de ordem. A v0.1 não persiste, deduplica ou ordena
eventos, e o status inicial existe apenas em `SendResult`.

## Brasil

Regras de sender, horário, operadora e delivery report mudam fora do ciclo de
release da biblioteca. Consulte regularmente as
[diretrizes oficiais da Twilio para SMS no Brasil](https://www.twilio.com/en-us/guidelines/br/sms).

## Fora da v0.1

Mensagens recebidas, MMS/mídia, lotes, agendamento, models, views prontas,
signals, async/Celery, retries, fallback entre provedores, normalização de
números e gerenciamento de templates não fazem parte desta versão.

O design completo está em
[`docs/superpowers/specs/2026-08-15-django-anysms-v01-design.md`](docs/superpowers/specs/2026-08-15-django-anysms-v01-design.md).

## Releases

O projeto usa [Release Please](https://github.com/googleapis/release-please)
com Conventional Commits. Pushes em `main` atualizam uma Release PR; ao
mesclá-la, o mesmo workflow cria a tag e a GitHub Release, constrói sdist e
wheel com Hatchling e publica no PyPI via Trusted Publisher (OIDC). Releases
criadas manualmente no GitHub não são publicadas.

Antes do primeiro release:

1. Em **Settings > Actions > General > Workflow permissions**, habilite
   **Allow GitHub Actions to create and approve pull requests**.
2. Crie o environment `pypi` em **Settings > Environments**. Proteções e
   aprovação manual são opcionais.
3. No PyPI, configure um
   [Trusted Publisher](https://docs.pypi.org/trusted-publishers/adding-a-publisher/)
   — ou um pending publisher para o primeiro upload — com estes valores:

   - Owner: `davisilvarafacho`
   - Repository: `django-anysms`
   - Workflow: `release.yml`
   - Environment: `pypi`

O workflow não usa `PYPI_API_TOKEN`. A primeira Release PR publica `v0.1.0`;
as próximas versões são calculadas a partir dos commits `feat`, `fix` e
mudanças incompatíveis.
