django-anysms 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. django_anysms-0.1.0/.github/dependabot.yml +6 -0
  2. django_anysms-0.1.0/.github/workflows/ci.yml +50 -0
  3. django_anysms-0.1.0/.github/workflows/release.yml +70 -0
  4. django_anysms-0.1.0/.gitignore +10 -0
  5. django_anysms-0.1.0/.release-please-manifest.json +1 -0
  6. django_anysms-0.1.0/CHANGELOG.md +31 -0
  7. django_anysms-0.1.0/PKG-INFO +237 -0
  8. django_anysms-0.1.0/README.md +218 -0
  9. django_anysms-0.1.0/django_anysms/__init__.py +17 -0
  10. django_anysms-0.1.0/django_anysms/base.py +35 -0
  11. django_anysms-0.1.0/django_anysms/conf.py +36 -0
  12. django_anysms-0.1.0/django_anysms/events.py +51 -0
  13. django_anysms-0.1.0/django_anysms/exceptions.py +52 -0
  14. django_anysms-0.1.0/django_anysms/messages.py +143 -0
  15. django_anysms-0.1.0/django_anysms/providers/__init__.py +1 -0
  16. django_anysms-0.1.0/django_anysms/providers/twilio/__init__.py +3 -0
  17. django_anysms-0.1.0/django_anysms/providers/twilio/provider.py +189 -0
  18. django_anysms-0.1.0/django_anysms/providers/twilio/sms.py +26 -0
  19. django_anysms-0.1.0/django_anysms/providers/twilio/webhooks.py +75 -0
  20. django_anysms-0.1.0/django_anysms/providers/twilio/whatsapp.py +52 -0
  21. django_anysms-0.1.0/django_anysms/registry.py +54 -0
  22. django_anysms-0.1.0/django_anysms/results.py +46 -0
  23. django_anysms-0.1.0/docs/research/twilio-v01-api-facts.md +337 -0
  24. django_anysms-0.1.0/docs/superpowers/plans/2026-08-15-django-anysms-v01.md +1025 -0
  25. django_anysms-0.1.0/docs/superpowers/plans/2026-08-16-release-automation.md +584 -0
  26. django_anysms-0.1.0/docs/superpowers/specs/2026-08-15-django-anysms-v01-design.md +386 -0
  27. django_anysms-0.1.0/docs/superpowers/specs/2026-08-16-release-automation-design.md +114 -0
  28. django_anysms-0.1.0/pyproject.toml +48 -0
  29. django_anysms-0.1.0/release-please-config.json +11 -0
  30. django_anysms-0.1.0/tests/__init__.py +1 -0
  31. django_anysms-0.1.0/tests/conftest.py +1 -0
  32. django_anysms-0.1.0/tests/fakes.py +62 -0
  33. django_anysms-0.1.0/tests/providers/__init__.py +1 -0
  34. django_anysms-0.1.0/tests/providers/twilio/__init__.py +1 -0
  35. django_anysms-0.1.0/tests/providers/twilio/fakes.py +55 -0
  36. django_anysms-0.1.0/tests/providers/twilio/test_provider.py +147 -0
  37. django_anysms-0.1.0/tests/providers/twilio/test_sms.py +119 -0
  38. django_anysms-0.1.0/tests/providers/twilio/test_webhooks.py +158 -0
  39. django_anysms-0.1.0/tests/providers/twilio/test_whatsapp.py +99 -0
  40. django_anysms-0.1.0/tests/settings.py +3 -0
  41. django_anysms-0.1.0/tests/test_messages.py +102 -0
  42. django_anysms-0.1.0/tests/test_package.py +34 -0
  43. django_anysms-0.1.0/tests/test_provider_contract.py +61 -0
  44. django_anysms-0.1.0/tests/test_public_api.py +18 -0
  45. django_anysms-0.1.0/tests/test_registry.py +83 -0
  46. django_anysms-0.1.0/tests/test_release_configuration.py +171 -0
  47. django_anysms-0.1.0/tests/test_types.py +63 -0
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
@@ -0,0 +1,50 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ include:
14
+ - {python: "3.10", django: "~=5.2.0"}
15
+ - {python: "3.11", django: "~=5.2.0"}
16
+ - {python: "3.12", django: "~=5.2.0"}
17
+ - {python: "3.12", django: "~=6.0.0"}
18
+ - {python: "3.13", django: "~=5.2.0"}
19
+ - {python: "3.13", django: "~=6.0.0"}
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: actions/setup-python@v5
23
+ with:
24
+ python-version: ${{ matrix.python }}
25
+ cache: pip
26
+ - run: python -m pip install -e '.[test,twilio]' "Django${{ matrix.django }}"
27
+ - run: python -m pytest -q
28
+ - run: python -m ruff check .
29
+ - run: python -m ruff format --check .
30
+ - run: python -m mypy django_anysms
31
+
32
+ build:
33
+ runs-on: ubuntu-latest
34
+ steps:
35
+ - uses: actions/checkout@v4
36
+ - uses: actions/setup-python@v5
37
+ with:
38
+ python-version: "3.13"
39
+ cache: pip
40
+ - run: python -m pip install build
41
+ - run: python -m build
42
+ - name: Verify core and Twilio extra installation
43
+ run: |
44
+ python -m venv /tmp/django-anysms-wheel
45
+ wheel=$(realpath "$(find dist -name '*.whl' -print -quit)")
46
+ /tmp/django-anysms-wheel/bin/python -m pip install "$wheel"
47
+ cd /tmp
48
+ /tmp/django-anysms-wheel/bin/python -c "import django_anysms"
49
+ /tmp/django-anysms-wheel/bin/python -m pip install "${wheel}[twilio]"
50
+ /tmp/django-anysms-wheel/bin/python -c "from django_anysms.providers.twilio import TwilioProvider"
@@ -0,0 +1,70 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions: {}
9
+
10
+ jobs:
11
+ release:
12
+ name: Release Please
13
+ runs-on: ubuntu-latest
14
+ permissions:
15
+ contents: write
16
+ issues: write
17
+ pull-requests: write
18
+ outputs:
19
+ release_created: ${{ steps.release.outputs.release_created }}
20
+ tag_name: ${{ steps.release.outputs.tag_name }}
21
+ steps:
22
+ - id: release
23
+ uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4
24
+ with:
25
+ config-file: release-please-config.json
26
+ manifest-file: .release-please-manifest.json
27
+
28
+ build:
29
+ name: Build distributions
30
+ needs: release
31
+ if: ${{ needs.release.outputs.release_created == 'true' }}
32
+ runs-on: ubuntu-latest
33
+ permissions:
34
+ contents: read
35
+ steps:
36
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
37
+ with:
38
+ ref: ${{ needs.release.outputs.tag_name }}
39
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
40
+ with:
41
+ python-version: "3.13"
42
+ - name: Install build tools
43
+ run: python -m pip install "build>=1.2,<2" "twine>=6,<7"
44
+ - name: Build distributions with Hatchling
45
+ run: python -m build
46
+ - name: Validate distributions
47
+ run: python -m twine check dist/*
48
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
49
+ with:
50
+ name: python-package-distributions
51
+ path: dist/
52
+ if-no-files-found: error
53
+
54
+ publish:
55
+ name: Publish distributions to PyPI
56
+ needs: build
57
+ runs-on: ubuntu-latest
58
+ environment:
59
+ name: pypi
60
+ url: https://pypi.org/p/django-anysms
61
+ permissions:
62
+ id-token: write
63
+ steps:
64
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
65
+ with:
66
+ name: python-package-distributions
67
+ path: dist/
68
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
69
+ with:
70
+ packages-dir: dist/
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .mypy_cache/
5
+ .ruff_cache/
6
+ .venv/
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+
@@ -0,0 +1 @@
1
+ {".":"0.1.0"}
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-09-05)
4
+
5
+
6
+ ### Features
7
+
8
+ * add normalized messaging value objects ([203e63c](https://github.com/davisilvarafacho/django-anysms/commit/203e63c8f58a2dbc370ff3816b8be8067c7e6dfc))
9
+ * add SMS and WhatsApp message contracts ([0d3eac3](https://github.com/davisilvarafacho/django-anysms/commit/0d3eac3be160de9981ced7e9576399b57d2c8534))
10
+ * add Twilio provider boundary ([7831691](https://github.com/davisilvarafacho/django-anysms/commit/78316913bdaa03bc3e69505d7ae93ff4f5fbba58))
11
+ * resolve providers from Django settings ([11efcbb](https://github.com/davisilvarafacho/django-anysms/commit/11efcbbc97d71a0fd47f2b7bac56928d934f3e92))
12
+ * send SMS messages through Twilio ([9ac8a00](https://github.com/davisilvarafacho/django-anysms/commit/9ac8a00f6f170b86a2b0b4907a8696045b207f8a))
13
+ * send WhatsApp messages through Twilio ([7f0eb2d](https://github.com/davisilvarafacho/django-anysms/commit/7f0eb2da5c869b243388355a0d91a559587d4fa1))
14
+ * validate and parse Twilio delivery callbacks ([85788cd](https://github.com/davisilvarafacho/django-anysms/commit/85788cd41234b046689388a30c09b85c126a6131))
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * close django-anysms v0.1 review gaps ([32a592c](https://github.com/davisilvarafacho/django-anysms/commit/32a592cdca6821f64a41a128490d0d4d7189db20))
20
+ * harden release automation ([28bd02a](https://github.com/davisilvarafacho/django-anysms/commit/28bd02aa7210857ca3169894a093bcf0d452cbf9))
21
+ * validate Twilio SID types before sending ([91418cb](https://github.com/davisilvarafacho/django-anysms/commit/91418cbe2b63622d9129c58bce71d539404587a9))
22
+
23
+
24
+ ### Documentation
25
+
26
+ * define django-anysms v0.1 design ([b049966](https://github.com/davisilvarafacho/django-anysms/commit/b049966365e894c3545259b20a8d06c3f124633b))
27
+ * design automated PyPI releases ([ea23409](https://github.com/davisilvarafacho/django-anysms/commit/ea23409b627fda31c69b6c8482f595dd921381ed))
28
+ * explain automated release setup ([29b2f33](https://github.com/davisilvarafacho/django-anysms/commit/29b2f33800fabdb63c9b09d8b74cd2d86c2f5764))
29
+ * plan automated PyPI releases ([e0523f2](https://github.com/davisilvarafacho/django-anysms/commit/e0523f275b492d236ba41c13745d273e6ea410a2))
30
+ * plan django-anysms v0.1 implementation ([693b1e7](https://github.com/davisilvarafacho/django-anysms/commit/693b1e7f6abc99f1cc0b4bfb2062b171686d08ff))
31
+ * publish django-anysms v0.1 usage and CI ([39aef43](https://github.com/davisilvarafacho/django-anysms/commit/39aef43b2f50c6efdbc3e2a484aaaf45cf8e06ae))
@@ -0,0 +1,237 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-anysms
3
+ Version: 0.1.0
4
+ Summary: Provider-independent SMS and WhatsApp messaging for Django
5
+ Requires-Python: <3.14,>=3.10
6
+ Requires-Dist: django<6.1,>=5.2
7
+ Provides-Extra: test
8
+ Requires-Dist: build>=1.2; extra == 'test'
9
+ Requires-Dist: mypy>=1.17; extra == 'test'
10
+ Requires-Dist: pytest-django>=4.11; extra == 'test'
11
+ Requires-Dist: pytest>=8.4; extra == 'test'
12
+ Requires-Dist: pyyaml<7,>=6.0; extra == 'test'
13
+ Requires-Dist: ruff>=0.12; extra == 'test'
14
+ Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'test'
15
+ Requires-Dist: twine<7,>=6; extra == 'test'
16
+ Provides-Extra: twilio
17
+ Requires-Dist: twilio<10,>=9.11; extra == 'twilio'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # django-anysms
21
+
22
+ Envio de SMS e WhatsApp com uma API Django consistente e provedores
23
+ intercambiáveis. A versão 0.1 inclui Twilio, mensagens síncronas e parsing
24
+ validado de callbacks de entrega.
25
+
26
+ ## Instalação
27
+
28
+ ```bash
29
+ python -m pip install django-anysms
30
+ python -m pip install 'django-anysms[twilio]'
31
+ ```
32
+
33
+ A v0.1 suporta Python 3.10–3.13, Django 5.2 LTS e Django 6.0. O SDK da
34
+ Twilio é opcional e não é importado pelo core.
35
+
36
+ ## Configuração
37
+
38
+ Configure aliases em suas settings Django:
39
+
40
+ ```python
41
+ import os
42
+
43
+ ANYSMS = {
44
+ "DEFAULT": "twilio",
45
+ "PROVIDERS": {
46
+ "twilio": {
47
+ "CLASS": "django_anysms.providers.twilio.TwilioProvider",
48
+ "OPTIONS": {
49
+ "account_sid": os.environ["TWILIO_ACCOUNT_SID"],
50
+ "auth_token": os.environ["TWILIO_AUTH_TOKEN"],
51
+ "from_": "+5511999999999",
52
+ },
53
+ },
54
+ },
55
+ }
56
+ ```
57
+
58
+ O pacote não precisa ser adicionado a `INSTALLED_APPS`.
59
+
60
+ ## SMS
61
+
62
+ ```python
63
+ from django_anysms import SMSMessage
64
+
65
+ result = SMSMessage(
66
+ to="+5511888888888",
67
+ body="Seu código é 123456",
68
+ status_callback="https://example.com/webhooks/twilio/status/",
69
+ ).send()
70
+
71
+ print(result.message_id, result.status)
72
+ ```
73
+
74
+ Cada mensagem possui exatamente um destinatário em formato E.164. Números
75
+ nacionais não são corrigidos ou completados automaticamente.
76
+
77
+ Um recurso específico pode ser informado sem contaminar a API comum:
78
+
79
+ ```python
80
+ message = SMSMessage(
81
+ to="+5511888888888",
82
+ body="Olá",
83
+ provider_options={
84
+ "twilio": {"messaging_service_sid": "MG..."},
85
+ },
86
+ )
87
+ message.send(using="twilio")
88
+ ```
89
+
90
+ ## WhatsApp
91
+
92
+ Texto livre dentro de uma conversa permitida pelo WhatsApp:
93
+
94
+ ```python
95
+ from django_anysms import WhatsAppMessage
96
+
97
+ WhatsAppMessage(
98
+ to="+5511888888888",
99
+ body="Seu pedido saiu para entrega.",
100
+ ).send()
101
+ ```
102
+
103
+ Mensagem proativa com um Content Template aprovado:
104
+
105
+ ```python
106
+ WhatsAppMessage(
107
+ to="+5511888888888",
108
+ content_sid="HX0123456789abcdef0123456789abcdef",
109
+ variables={"1": "Rafael", "2": "15/08/2026"},
110
+ ).send()
111
+ ```
112
+
113
+ `body` e `content_sid` são mutuamente exclusivos. O provider adiciona o
114
+ prefixo `whatsapp:`; a API pública recebe somente E.164.
115
+
116
+ ## Provider explícito
117
+
118
+ Settings são opcionais quando a aplicação precisa de credenciais dinâmicas:
119
+
120
+ ```python
121
+ from django_anysms import SMSMessage
122
+ from django_anysms.providers.twilio import TwilioProvider
123
+
124
+ provider = TwilioProvider(
125
+ account_sid="AC...",
126
+ auth_token="...",
127
+ from_="+5511999999999",
128
+ )
129
+
130
+ result = SMSMessage(to="+5511888888888", body="Olá").send(provider=provider)
131
+ ```
132
+
133
+ A prioridade é: instância em `provider=`, alias em `using=` e, por último,
134
+ `ANYSMS["DEFAULT"]`. `provider` e `using` não podem ser usados juntos.
135
+
136
+ ## Erros
137
+
138
+ Falhas esperadas herdam de `AnySMSError`. Por padrão elas são lançadas. Para
139
+ um fluxo compatível com `fail_silently`, use:
140
+
141
+ ```python
142
+ result = message.send(fail_silently=True)
143
+ if not result.accepted:
144
+ report(result.error)
145
+ ```
146
+
147
+ `accepted=True` significa que o provedor aceitou a chamada, não que a
148
+ mensagem chegou ao aparelho. Não há retry automático para evitar duplicatas
149
+ após falhas ambíguas.
150
+
151
+ A hierarquia pública inclui `ConfigurationError`, `MessageValidationError`,
152
+ `ProviderNotInstalledError`, `ProviderError`, `InvalidWebhookSignature` e
153
+ `InvalidWebhookPayload`, todos em `django_anysms.exceptions`.
154
+
155
+ ## Callback de entrega
156
+
157
+ A aplicação controla sua própria URL e persistência. Exemplo de view:
158
+
159
+ ```python
160
+ from django.http import HttpRequest, HttpResponse
161
+ from django.views.decorators.csrf import csrf_exempt
162
+
163
+ from django_anysms.exceptions import InvalidWebhookPayload, InvalidWebhookSignature
164
+ from django_anysms.registry import get_provider
165
+
166
+
167
+ @csrf_exempt
168
+ def twilio_status(request: HttpRequest) -> HttpResponse:
169
+ provider = get_provider("twilio")
170
+ try:
171
+ event = provider.parse_webhook(
172
+ public_url=request.build_absolute_uri(),
173
+ form=request.POST,
174
+ signature=request.headers.get("X-Twilio-Signature"),
175
+ )
176
+ except InvalidWebhookSignature:
177
+ return HttpResponse(status=403)
178
+ except InvalidWebhookPayload:
179
+ return HttpResponse(status=400)
180
+
181
+ consume_delivery_event(event)
182
+ return HttpResponse(status=204)
183
+ ```
184
+
185
+ Em ambientes com proxy, `public_url` precisa ser exatamente a URL pública
186
+ assinada pela Twilio. A biblioteca valida a assinatura antes de projetar o
187
+ payload.
188
+
189
+ `DeliveryEvent.raw` preserva o formulário decodificado completo, inclusive
190
+ campos desconhecidos e valores repetidos. Tanto ele quanto `SendResult.raw`
191
+ podem conter telefones e outros dados pessoais; não registre ou persista esses
192
+ mappings sem uma política adequada.
193
+
194
+ Callbacks podem chegar fora de ordem. A v0.1 não persiste, deduplica ou ordena
195
+ eventos, e o status inicial existe apenas em `SendResult`.
196
+
197
+ ## Brasil
198
+
199
+ Regras de sender, horário, operadora e delivery report mudam fora do ciclo de
200
+ release da biblioteca. Consulte regularmente as
201
+ [diretrizes oficiais da Twilio para SMS no Brasil](https://www.twilio.com/en-us/guidelines/br/sms).
202
+
203
+ ## Fora da v0.1
204
+
205
+ Mensagens recebidas, MMS/mídia, lotes, agendamento, models, views prontas,
206
+ signals, async/Celery, retries, fallback entre provedores, normalização de
207
+ números e gerenciamento de templates não fazem parte desta versão.
208
+
209
+ O design completo está em
210
+ [`docs/superpowers/specs/2026-08-15-django-anysms-v01-design.md`](docs/superpowers/specs/2026-08-15-django-anysms-v01-design.md).
211
+
212
+ ## Releases
213
+
214
+ O projeto usa [Release Please](https://github.com/googleapis/release-please)
215
+ com Conventional Commits. Pushes em `main` atualizam uma Release PR; ao
216
+ mesclá-la, o mesmo workflow cria a tag e a GitHub Release, constrói sdist e
217
+ wheel com Hatchling e publica no PyPI via Trusted Publisher (OIDC). Releases
218
+ criadas manualmente no GitHub não são publicadas.
219
+
220
+ Antes do primeiro release:
221
+
222
+ 1. Em **Settings > Actions > General > Workflow permissions**, habilite
223
+ **Allow GitHub Actions to create and approve pull requests**.
224
+ 2. Crie o environment `pypi` em **Settings > Environments**. Proteções e
225
+ aprovação manual são opcionais.
226
+ 3. No PyPI, configure um
227
+ [Trusted Publisher](https://docs.pypi.org/trusted-publishers/adding-a-publisher/)
228
+ — ou um pending publisher para o primeiro upload — com estes valores:
229
+
230
+ - Owner: `davisilvarafacho`
231
+ - Repository: `django-anysms`
232
+ - Workflow: `release.yml`
233
+ - Environment: `pypi`
234
+
235
+ O workflow não usa `PYPI_API_TOKEN`. A primeira Release PR publica `v0.1.0`;
236
+ as próximas versões são calculadas a partir dos commits `feat`, `fix` e
237
+ mudanças incompatíveis.
@@ -0,0 +1,218 @@
1
+ # django-anysms
2
+
3
+ Envio de SMS e WhatsApp com uma API Django consistente e provedores
4
+ intercambiáveis. A versão 0.1 inclui Twilio, mensagens síncronas e parsing
5
+ validado de callbacks de entrega.
6
+
7
+ ## Instalação
8
+
9
+ ```bash
10
+ python -m pip install django-anysms
11
+ python -m pip install 'django-anysms[twilio]'
12
+ ```
13
+
14
+ A v0.1 suporta Python 3.10–3.13, Django 5.2 LTS e Django 6.0. O SDK da
15
+ Twilio é opcional e não é importado pelo core.
16
+
17
+ ## Configuração
18
+
19
+ Configure aliases em suas settings Django:
20
+
21
+ ```python
22
+ import os
23
+
24
+ ANYSMS = {
25
+ "DEFAULT": "twilio",
26
+ "PROVIDERS": {
27
+ "twilio": {
28
+ "CLASS": "django_anysms.providers.twilio.TwilioProvider",
29
+ "OPTIONS": {
30
+ "account_sid": os.environ["TWILIO_ACCOUNT_SID"],
31
+ "auth_token": os.environ["TWILIO_AUTH_TOKEN"],
32
+ "from_": "+5511999999999",
33
+ },
34
+ },
35
+ },
36
+ }
37
+ ```
38
+
39
+ O pacote não precisa ser adicionado a `INSTALLED_APPS`.
40
+
41
+ ## SMS
42
+
43
+ ```python
44
+ from django_anysms import SMSMessage
45
+
46
+ result = SMSMessage(
47
+ to="+5511888888888",
48
+ body="Seu código é 123456",
49
+ status_callback="https://example.com/webhooks/twilio/status/",
50
+ ).send()
51
+
52
+ print(result.message_id, result.status)
53
+ ```
54
+
55
+ Cada mensagem possui exatamente um destinatário em formato E.164. Números
56
+ nacionais não são corrigidos ou completados automaticamente.
57
+
58
+ Um recurso específico pode ser informado sem contaminar a API comum:
59
+
60
+ ```python
61
+ message = SMSMessage(
62
+ to="+5511888888888",
63
+ body="Olá",
64
+ provider_options={
65
+ "twilio": {"messaging_service_sid": "MG..."},
66
+ },
67
+ )
68
+ message.send(using="twilio")
69
+ ```
70
+
71
+ ## WhatsApp
72
+
73
+ Texto livre dentro de uma conversa permitida pelo WhatsApp:
74
+
75
+ ```python
76
+ from django_anysms import WhatsAppMessage
77
+
78
+ WhatsAppMessage(
79
+ to="+5511888888888",
80
+ body="Seu pedido saiu para entrega.",
81
+ ).send()
82
+ ```
83
+
84
+ Mensagem proativa com um Content Template aprovado:
85
+
86
+ ```python
87
+ WhatsAppMessage(
88
+ to="+5511888888888",
89
+ content_sid="HX0123456789abcdef0123456789abcdef",
90
+ variables={"1": "Rafael", "2": "15/08/2026"},
91
+ ).send()
92
+ ```
93
+
94
+ `body` e `content_sid` são mutuamente exclusivos. O provider adiciona o
95
+ prefixo `whatsapp:`; a API pública recebe somente E.164.
96
+
97
+ ## Provider explícito
98
+
99
+ Settings são opcionais quando a aplicação precisa de credenciais dinâmicas:
100
+
101
+ ```python
102
+ from django_anysms import SMSMessage
103
+ from django_anysms.providers.twilio import TwilioProvider
104
+
105
+ provider = TwilioProvider(
106
+ account_sid="AC...",
107
+ auth_token="...",
108
+ from_="+5511999999999",
109
+ )
110
+
111
+ result = SMSMessage(to="+5511888888888", body="Olá").send(provider=provider)
112
+ ```
113
+
114
+ A prioridade é: instância em `provider=`, alias em `using=` e, por último,
115
+ `ANYSMS["DEFAULT"]`. `provider` e `using` não podem ser usados juntos.
116
+
117
+ ## Erros
118
+
119
+ Falhas esperadas herdam de `AnySMSError`. Por padrão elas são lançadas. Para
120
+ um fluxo compatível com `fail_silently`, use:
121
+
122
+ ```python
123
+ result = message.send(fail_silently=True)
124
+ if not result.accepted:
125
+ report(result.error)
126
+ ```
127
+
128
+ `accepted=True` significa que o provedor aceitou a chamada, não que a
129
+ mensagem chegou ao aparelho. Não há retry automático para evitar duplicatas
130
+ após falhas ambíguas.
131
+
132
+ A hierarquia pública inclui `ConfigurationError`, `MessageValidationError`,
133
+ `ProviderNotInstalledError`, `ProviderError`, `InvalidWebhookSignature` e
134
+ `InvalidWebhookPayload`, todos em `django_anysms.exceptions`.
135
+
136
+ ## Callback de entrega
137
+
138
+ A aplicação controla sua própria URL e persistência. Exemplo de view:
139
+
140
+ ```python
141
+ from django.http import HttpRequest, HttpResponse
142
+ from django.views.decorators.csrf import csrf_exempt
143
+
144
+ from django_anysms.exceptions import InvalidWebhookPayload, InvalidWebhookSignature
145
+ from django_anysms.registry import get_provider
146
+
147
+
148
+ @csrf_exempt
149
+ def twilio_status(request: HttpRequest) -> HttpResponse:
150
+ provider = get_provider("twilio")
151
+ try:
152
+ event = provider.parse_webhook(
153
+ public_url=request.build_absolute_uri(),
154
+ form=request.POST,
155
+ signature=request.headers.get("X-Twilio-Signature"),
156
+ )
157
+ except InvalidWebhookSignature:
158
+ return HttpResponse(status=403)
159
+ except InvalidWebhookPayload:
160
+ return HttpResponse(status=400)
161
+
162
+ consume_delivery_event(event)
163
+ return HttpResponse(status=204)
164
+ ```
165
+
166
+ Em ambientes com proxy, `public_url` precisa ser exatamente a URL pública
167
+ assinada pela Twilio. A biblioteca valida a assinatura antes de projetar o
168
+ payload.
169
+
170
+ `DeliveryEvent.raw` preserva o formulário decodificado completo, inclusive
171
+ campos desconhecidos e valores repetidos. Tanto ele quanto `SendResult.raw`
172
+ podem conter telefones e outros dados pessoais; não registre ou persista esses
173
+ mappings sem uma política adequada.
174
+
175
+ Callbacks podem chegar fora de ordem. A v0.1 não persiste, deduplica ou ordena
176
+ eventos, e o status inicial existe apenas em `SendResult`.
177
+
178
+ ## Brasil
179
+
180
+ Regras de sender, horário, operadora e delivery report mudam fora do ciclo de
181
+ release da biblioteca. Consulte regularmente as
182
+ [diretrizes oficiais da Twilio para SMS no Brasil](https://www.twilio.com/en-us/guidelines/br/sms).
183
+
184
+ ## Fora da v0.1
185
+
186
+ Mensagens recebidas, MMS/mídia, lotes, agendamento, models, views prontas,
187
+ signals, async/Celery, retries, fallback entre provedores, normalização de
188
+ números e gerenciamento de templates não fazem parte desta versão.
189
+
190
+ O design completo está em
191
+ [`docs/superpowers/specs/2026-08-15-django-anysms-v01-design.md`](docs/superpowers/specs/2026-08-15-django-anysms-v01-design.md).
192
+
193
+ ## Releases
194
+
195
+ O projeto usa [Release Please](https://github.com/googleapis/release-please)
196
+ com Conventional Commits. Pushes em `main` atualizam uma Release PR; ao
197
+ mesclá-la, o mesmo workflow cria a tag e a GitHub Release, constrói sdist e
198
+ wheel com Hatchling e publica no PyPI via Trusted Publisher (OIDC). Releases
199
+ criadas manualmente no GitHub não são publicadas.
200
+
201
+ Antes do primeiro release:
202
+
203
+ 1. Em **Settings > Actions > General > Workflow permissions**, habilite
204
+ **Allow GitHub Actions to create and approve pull requests**.
205
+ 2. Crie o environment `pypi` em **Settings > Environments**. Proteções e
206
+ aprovação manual são opcionais.
207
+ 3. No PyPI, configure um
208
+ [Trusted Publisher](https://docs.pypi.org/trusted-publishers/adding-a-publisher/)
209
+ — ou um pending publisher para o primeiro upload — com estes valores:
210
+
211
+ - Owner: `davisilvarafacho`
212
+ - Repository: `django-anysms`
213
+ - Workflow: `release.yml`
214
+ - Environment: `pypi`
215
+
216
+ O workflow não usa `PYPI_API_TOKEN`. A primeira Release PR publica `v0.1.0`;
217
+ as próximas versões são calculadas a partir dos commits `feat`, `fix` e
218
+ mudanças incompatíveis.
@@ -0,0 +1,17 @@
1
+ from .events import Channel, DeliveryEvent, MessageStatus
2
+ from .exceptions import AnySMSError
3
+ from .messages import SMSMessage, WhatsAppMessage
4
+ from .results import SendResult
5
+
6
+ __version__ = "0.1.0"
7
+
8
+ __all__ = [
9
+ "AnySMSError",
10
+ "Channel",
11
+ "DeliveryEvent",
12
+ "MessageStatus",
13
+ "SMSMessage",
14
+ "SendResult",
15
+ "WhatsAppMessage",
16
+ "__version__",
17
+ ]
@@ -0,0 +1,35 @@
1
+ from abc import ABC, abstractmethod
2
+ from collections.abc import Iterator
3
+ from typing import TYPE_CHECKING, Protocol
4
+
5
+ if TYPE_CHECKING:
6
+ from .events import DeliveryEvent
7
+ from .messages import SMSMessage, WhatsAppMessage
8
+ from .results import SendResult
9
+
10
+
11
+ class MultiValueForm(Protocol):
12
+ def __iter__(self) -> Iterator[str]: ...
13
+
14
+ def getlist(self, key: str) -> list[str]: ...
15
+
16
+ def get(self, key: str, default: str | None = None) -> str | None: ...
17
+
18
+
19
+ class BaseProvider(ABC):
20
+ name: str
21
+
22
+ @abstractmethod
23
+ def send_sms(self, message: "SMSMessage") -> "SendResult": ...
24
+
25
+ @abstractmethod
26
+ def send_whatsapp(self, message: "WhatsAppMessage") -> "SendResult": ...
27
+
28
+ @abstractmethod
29
+ def parse_webhook(
30
+ self,
31
+ *,
32
+ public_url: str,
33
+ form: MultiValueForm,
34
+ signature: str | None,
35
+ ) -> "DeliveryEvent": ...