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.
- django_anysms-0.1.0/.github/dependabot.yml +6 -0
- django_anysms-0.1.0/.github/workflows/ci.yml +50 -0
- django_anysms-0.1.0/.github/workflows/release.yml +70 -0
- django_anysms-0.1.0/.gitignore +10 -0
- django_anysms-0.1.0/.release-please-manifest.json +1 -0
- django_anysms-0.1.0/CHANGELOG.md +31 -0
- django_anysms-0.1.0/PKG-INFO +237 -0
- django_anysms-0.1.0/README.md +218 -0
- django_anysms-0.1.0/django_anysms/__init__.py +17 -0
- django_anysms-0.1.0/django_anysms/base.py +35 -0
- django_anysms-0.1.0/django_anysms/conf.py +36 -0
- django_anysms-0.1.0/django_anysms/events.py +51 -0
- django_anysms-0.1.0/django_anysms/exceptions.py +52 -0
- django_anysms-0.1.0/django_anysms/messages.py +143 -0
- django_anysms-0.1.0/django_anysms/providers/__init__.py +1 -0
- django_anysms-0.1.0/django_anysms/providers/twilio/__init__.py +3 -0
- django_anysms-0.1.0/django_anysms/providers/twilio/provider.py +189 -0
- django_anysms-0.1.0/django_anysms/providers/twilio/sms.py +26 -0
- django_anysms-0.1.0/django_anysms/providers/twilio/webhooks.py +75 -0
- django_anysms-0.1.0/django_anysms/providers/twilio/whatsapp.py +52 -0
- django_anysms-0.1.0/django_anysms/registry.py +54 -0
- django_anysms-0.1.0/django_anysms/results.py +46 -0
- django_anysms-0.1.0/docs/research/twilio-v01-api-facts.md +337 -0
- django_anysms-0.1.0/docs/superpowers/plans/2026-08-15-django-anysms-v01.md +1025 -0
- django_anysms-0.1.0/docs/superpowers/plans/2026-08-16-release-automation.md +584 -0
- django_anysms-0.1.0/docs/superpowers/specs/2026-08-15-django-anysms-v01-design.md +386 -0
- django_anysms-0.1.0/docs/superpowers/specs/2026-08-16-release-automation-design.md +114 -0
- django_anysms-0.1.0/pyproject.toml +48 -0
- django_anysms-0.1.0/release-please-config.json +11 -0
- django_anysms-0.1.0/tests/__init__.py +1 -0
- django_anysms-0.1.0/tests/conftest.py +1 -0
- django_anysms-0.1.0/tests/fakes.py +62 -0
- django_anysms-0.1.0/tests/providers/__init__.py +1 -0
- django_anysms-0.1.0/tests/providers/twilio/__init__.py +1 -0
- django_anysms-0.1.0/tests/providers/twilio/fakes.py +55 -0
- django_anysms-0.1.0/tests/providers/twilio/test_provider.py +147 -0
- django_anysms-0.1.0/tests/providers/twilio/test_sms.py +119 -0
- django_anysms-0.1.0/tests/providers/twilio/test_webhooks.py +158 -0
- django_anysms-0.1.0/tests/providers/twilio/test_whatsapp.py +99 -0
- django_anysms-0.1.0/tests/settings.py +3 -0
- django_anysms-0.1.0/tests/test_messages.py +102 -0
- django_anysms-0.1.0/tests/test_package.py +34 -0
- django_anysms-0.1.0/tests/test_provider_contract.py +61 -0
- django_anysms-0.1.0/tests/test_public_api.py +18 -0
- django_anysms-0.1.0/tests/test_registry.py +83 -0
- django_anysms-0.1.0/tests/test_release_configuration.py +171 -0
- django_anysms-0.1.0/tests/test_types.py +63 -0
|
@@ -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 @@
|
|
|
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": ...
|