mojawave 1.0.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.
- mojawave-1.0.0/.claude/settings.local.json +9 -0
- mojawave-1.0.0/.gitignore +13 -0
- mojawave-1.0.0/LICENSE +21 -0
- mojawave-1.0.0/PKG-INFO +211 -0
- mojawave-1.0.0/README.md +180 -0
- mojawave-1.0.0/examples/flask_webhook.py +37 -0
- mojawave-1.0.0/examples/quickstart.py +33 -0
- mojawave-1.0.0/pyproject.toml +46 -0
- mojawave-1.0.0/src/mojawave/__init__.py +70 -0
- mojawave-1.0.0/src/mojawave/_transport.py +207 -0
- mojawave-1.0.0/src/mojawave/client.py +87 -0
- mojawave-1.0.0/src/mojawave/errors.py +129 -0
- mojawave-1.0.0/src/mojawave/models.py +190 -0
- mojawave-1.0.0/src/mojawave/py.typed +0 -0
- mojawave-1.0.0/src/mojawave/resources/__init__.py +4 -0
- mojawave-1.0.0/src/mojawave/resources/credits.py +17 -0
- mojawave-1.0.0/src/mojawave/resources/sms.py +117 -0
- mojawave-1.0.0/src/mojawave/webhooks.py +102 -0
- mojawave-1.0.0/tests/conftest.py +8 -0
- mojawave-1.0.0/tests/test_errors_and_credits.py +113 -0
- mojawave-1.0.0/tests/test_sms.py +137 -0
- mojawave-1.0.0/tests/test_webhooks.py +47 -0
mojawave-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MojaWave
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mojawave-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mojawave
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for the MojaWave API — SMS and transactional messaging for Tanzania.
|
|
5
|
+
Project-URL: Homepage, https://mojawave.com
|
|
6
|
+
Project-URL: Documentation, https://mojawave.com/docs
|
|
7
|
+
Project-URL: Source, https://github.com/mojawave/mojawave-python
|
|
8
|
+
Author: MojaWave
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: api,messaging,mojawave,otp,sdk,sms,tanzania
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Requires-Dist: requests>=2.25
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: responses>=0.23; extra == 'dev'
|
|
29
|
+
Requires-Dist: types-requests; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# MojaWave Python SDK
|
|
33
|
+
|
|
34
|
+
A thin, typed Python client for the [MojaWave](https://mojawave.com) REST API —
|
|
35
|
+
send SMS (single, bulk, OTP), check credit balances, and verify webhooks across
|
|
36
|
+
Tanzania's telco networks (Vodacom, Tigo, Airtel, Halotel).
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install mojawave
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Requires Python 3.8+.
|
|
43
|
+
|
|
44
|
+
## Quickstart
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from mojawave import MojaWave
|
|
48
|
+
|
|
49
|
+
client = MojaWave(api_key="sk_live_mw_...") # or set MOJAWAVE_API_KEY
|
|
50
|
+
|
|
51
|
+
msg = client.sms.send(
|
|
52
|
+
to="+255753276939",
|
|
53
|
+
sender="MojaWave",
|
|
54
|
+
message="Hello from Mojawave! Your verification code is 1234.",
|
|
55
|
+
)
|
|
56
|
+
print(msg.id, msg.status)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The client reads `MOJAWAVE_API_KEY` from the environment when `api_key` is
|
|
60
|
+
omitted. Use an `sk_test_mw_` key for the sandbox — no real messages are sent
|
|
61
|
+
and no charges apply.
|
|
62
|
+
|
|
63
|
+
> **Never** expose your live API key in client-side code. Use environment
|
|
64
|
+
> variables and server-side requests only.
|
|
65
|
+
|
|
66
|
+
## Sending SMS
|
|
67
|
+
|
|
68
|
+
### Single message
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
msg = client.sms.send(
|
|
72
|
+
to="+255712345678",
|
|
73
|
+
sender="MojaWave", # sender ID (≤11 alphanumeric chars); defaults to MojaWave
|
|
74
|
+
message="Your code is 1234.",
|
|
75
|
+
webhook_url="https://example.com/webhooks/sms", # optional delivery receipts
|
|
76
|
+
schedule_at="2026-07-01T09:00:00Z", # optional ISO-8601 schedule
|
|
77
|
+
metadata={"customer_id": "cust98765"}, # optional, echoed back
|
|
78
|
+
tags=["onboarding", "verification"], # optional
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Look up a message
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
msg = client.sms.get("89b82624-f1a2-4f5e-85b5-102e79a06779")
|
|
86
|
+
if msg.delivered:
|
|
87
|
+
print("Delivered at", msg.timeline.delivered_at)
|
|
88
|
+
elif msg.failed:
|
|
89
|
+
print("Failed:", msg.failure_reason)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Bulk send (up to 10,000 recipients)
|
|
93
|
+
|
|
94
|
+
Bulk jobs run asynchronously — you get a job back immediately, then poll it.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
job = client.sms.bulk(
|
|
98
|
+
name="Marketing Campaign Q1",
|
|
99
|
+
sender="MojaWave",
|
|
100
|
+
message="Hello {name}, your code is {code}",
|
|
101
|
+
recipients=[
|
|
102
|
+
{"to": "+255712345678", "personalization": {"name": "John", "code": "ABC123"}},
|
|
103
|
+
{"to": "+255712345679", "personalization": {"name": "Jane", "code": "XYZ789"}},
|
|
104
|
+
"+255712345680", # a bare string works too (no personalization)
|
|
105
|
+
],
|
|
106
|
+
webhook_url="https://example.com/webhooks",
|
|
107
|
+
)
|
|
108
|
+
print(job.id, job.status, job.total_recipients)
|
|
109
|
+
|
|
110
|
+
# Poll for progress
|
|
111
|
+
job = client.sms.get_bulk(job.id)
|
|
112
|
+
print(f"{job.progress_percent}% — {job.sent_count} sent")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
> Unicode messages have a 70-character per-segment limit (vs. 160 for plain
|
|
116
|
+
> SMS). Plan message length accordingly.
|
|
117
|
+
|
|
118
|
+
## Credits
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
balances = client.credits.balance()
|
|
122
|
+
print(balances.sms.balance, balances.sms.is_low_balance)
|
|
123
|
+
print(balances.email.balance)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Webhooks
|
|
127
|
+
|
|
128
|
+
MojaWave signs every webhook with an `X-MojaWave-Signature` header (HMAC-SHA256
|
|
129
|
+
of the raw body). Always verify against the **raw** request bytes — parsing to
|
|
130
|
+
JSON first can change whitespace and break the check.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from mojawave import construct_event, WebhookVerificationError, SIGNATURE_HEADER
|
|
134
|
+
|
|
135
|
+
# Flask / Django view
|
|
136
|
+
signature = request.headers.get(SIGNATURE_HEADER)
|
|
137
|
+
try:
|
|
138
|
+
event = construct_event(request.get_data(), signature, WEBHOOK_SECRET)
|
|
139
|
+
except WebhookVerificationError:
|
|
140
|
+
return "Forbidden", 403
|
|
141
|
+
|
|
142
|
+
if event.type == "message.delivered":
|
|
143
|
+
...
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Event types: `message.sent`, `message.delivered`, `message.failed`,
|
|
147
|
+
`credits.low`. See `examples/flask_webhook.py` for a full handler.
|
|
148
|
+
|
|
149
|
+
If you only need a boolean, use `verify_signature(payload, signature, secret)`.
|
|
150
|
+
|
|
151
|
+
## Error handling
|
|
152
|
+
|
|
153
|
+
Every documented HTTP status maps to a typed exception. All inherit from
|
|
154
|
+
`MojaWaveError`.
|
|
155
|
+
|
|
156
|
+
| Exception | HTTP | Code |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| `InvalidRequestError` | 400 | `invalid_request` |
|
|
159
|
+
| `AuthenticationError` | 401 | `unauthorized` |
|
|
160
|
+
| `InsufficientBalanceError` | 402 | `insufficient_balance` |
|
|
161
|
+
| `UnprocessableError` | 422 | `unprocessable` |
|
|
162
|
+
| `RateLimitError` | 429 | `rate_limit_exceeded` |
|
|
163
|
+
| `ServerError` | 5xx | `server_error` |
|
|
164
|
+
| `APIConnectionError` / `APITimeoutError` | — | transport failures |
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from mojawave import InsufficientBalanceError, RateLimitError
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
client.sms.send(to="+255712345678", message="hi")
|
|
171
|
+
except InsufficientBalanceError:
|
|
172
|
+
... # top up
|
|
173
|
+
except RateLimitError as e:
|
|
174
|
+
time.sleep(e.retry_after or 1)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The client automatically retries `429` and `5xx` responses with exponential
|
|
178
|
+
backoff (honouring `Retry-After`), controlled by `max_retries` (default 2).
|
|
179
|
+
|
|
180
|
+
## Configuration
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
client = MojaWave(
|
|
184
|
+
api_key="sk_live_mw_...",
|
|
185
|
+
environment="live", # or "sandbox"
|
|
186
|
+
timeout=30.0, # seconds
|
|
187
|
+
max_retries=2,
|
|
188
|
+
base_url="https://api.mojawave.com/v1",
|
|
189
|
+
)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Rate-limit headers from the most recent response are available on
|
|
193
|
+
`client.rate_limit` (`.limit`, `.remaining`, `.reset`). The client is usable as
|
|
194
|
+
a context manager to ensure the HTTP session is closed:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
with MojaWave() as client:
|
|
198
|
+
client.sms.send(to="+255712345678", message="hi")
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Development
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
pip install -e ".[dev]"
|
|
205
|
+
pytest
|
|
206
|
+
mypy
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT
|
mojawave-1.0.0/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# MojaWave Python SDK
|
|
2
|
+
|
|
3
|
+
A thin, typed Python client for the [MojaWave](https://mojawave.com) REST API —
|
|
4
|
+
send SMS (single, bulk, OTP), check credit balances, and verify webhooks across
|
|
5
|
+
Tanzania's telco networks (Vodacom, Tigo, Airtel, Halotel).
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install mojawave
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Python 3.8+.
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from mojawave import MojaWave
|
|
17
|
+
|
|
18
|
+
client = MojaWave(api_key="sk_live_mw_...") # or set MOJAWAVE_API_KEY
|
|
19
|
+
|
|
20
|
+
msg = client.sms.send(
|
|
21
|
+
to="+255753276939",
|
|
22
|
+
sender="MojaWave",
|
|
23
|
+
message="Hello from Mojawave! Your verification code is 1234.",
|
|
24
|
+
)
|
|
25
|
+
print(msg.id, msg.status)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The client reads `MOJAWAVE_API_KEY` from the environment when `api_key` is
|
|
29
|
+
omitted. Use an `sk_test_mw_` key for the sandbox — no real messages are sent
|
|
30
|
+
and no charges apply.
|
|
31
|
+
|
|
32
|
+
> **Never** expose your live API key in client-side code. Use environment
|
|
33
|
+
> variables and server-side requests only.
|
|
34
|
+
|
|
35
|
+
## Sending SMS
|
|
36
|
+
|
|
37
|
+
### Single message
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
msg = client.sms.send(
|
|
41
|
+
to="+255712345678",
|
|
42
|
+
sender="MojaWave", # sender ID (≤11 alphanumeric chars); defaults to MojaWave
|
|
43
|
+
message="Your code is 1234.",
|
|
44
|
+
webhook_url="https://example.com/webhooks/sms", # optional delivery receipts
|
|
45
|
+
schedule_at="2026-07-01T09:00:00Z", # optional ISO-8601 schedule
|
|
46
|
+
metadata={"customer_id": "cust98765"}, # optional, echoed back
|
|
47
|
+
tags=["onboarding", "verification"], # optional
|
|
48
|
+
)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Look up a message
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
msg = client.sms.get("89b82624-f1a2-4f5e-85b5-102e79a06779")
|
|
55
|
+
if msg.delivered:
|
|
56
|
+
print("Delivered at", msg.timeline.delivered_at)
|
|
57
|
+
elif msg.failed:
|
|
58
|
+
print("Failed:", msg.failure_reason)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Bulk send (up to 10,000 recipients)
|
|
62
|
+
|
|
63
|
+
Bulk jobs run asynchronously — you get a job back immediately, then poll it.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
job = client.sms.bulk(
|
|
67
|
+
name="Marketing Campaign Q1",
|
|
68
|
+
sender="MojaWave",
|
|
69
|
+
message="Hello {name}, your code is {code}",
|
|
70
|
+
recipients=[
|
|
71
|
+
{"to": "+255712345678", "personalization": {"name": "John", "code": "ABC123"}},
|
|
72
|
+
{"to": "+255712345679", "personalization": {"name": "Jane", "code": "XYZ789"}},
|
|
73
|
+
"+255712345680", # a bare string works too (no personalization)
|
|
74
|
+
],
|
|
75
|
+
webhook_url="https://example.com/webhooks",
|
|
76
|
+
)
|
|
77
|
+
print(job.id, job.status, job.total_recipients)
|
|
78
|
+
|
|
79
|
+
# Poll for progress
|
|
80
|
+
job = client.sms.get_bulk(job.id)
|
|
81
|
+
print(f"{job.progress_percent}% — {job.sent_count} sent")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
> Unicode messages have a 70-character per-segment limit (vs. 160 for plain
|
|
85
|
+
> SMS). Plan message length accordingly.
|
|
86
|
+
|
|
87
|
+
## Credits
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
balances = client.credits.balance()
|
|
91
|
+
print(balances.sms.balance, balances.sms.is_low_balance)
|
|
92
|
+
print(balances.email.balance)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Webhooks
|
|
96
|
+
|
|
97
|
+
MojaWave signs every webhook with an `X-MojaWave-Signature` header (HMAC-SHA256
|
|
98
|
+
of the raw body). Always verify against the **raw** request bytes — parsing to
|
|
99
|
+
JSON first can change whitespace and break the check.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from mojawave import construct_event, WebhookVerificationError, SIGNATURE_HEADER
|
|
103
|
+
|
|
104
|
+
# Flask / Django view
|
|
105
|
+
signature = request.headers.get(SIGNATURE_HEADER)
|
|
106
|
+
try:
|
|
107
|
+
event = construct_event(request.get_data(), signature, WEBHOOK_SECRET)
|
|
108
|
+
except WebhookVerificationError:
|
|
109
|
+
return "Forbidden", 403
|
|
110
|
+
|
|
111
|
+
if event.type == "message.delivered":
|
|
112
|
+
...
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Event types: `message.sent`, `message.delivered`, `message.failed`,
|
|
116
|
+
`credits.low`. See `examples/flask_webhook.py` for a full handler.
|
|
117
|
+
|
|
118
|
+
If you only need a boolean, use `verify_signature(payload, signature, secret)`.
|
|
119
|
+
|
|
120
|
+
## Error handling
|
|
121
|
+
|
|
122
|
+
Every documented HTTP status maps to a typed exception. All inherit from
|
|
123
|
+
`MojaWaveError`.
|
|
124
|
+
|
|
125
|
+
| Exception | HTTP | Code |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| `InvalidRequestError` | 400 | `invalid_request` |
|
|
128
|
+
| `AuthenticationError` | 401 | `unauthorized` |
|
|
129
|
+
| `InsufficientBalanceError` | 402 | `insufficient_balance` |
|
|
130
|
+
| `UnprocessableError` | 422 | `unprocessable` |
|
|
131
|
+
| `RateLimitError` | 429 | `rate_limit_exceeded` |
|
|
132
|
+
| `ServerError` | 5xx | `server_error` |
|
|
133
|
+
| `APIConnectionError` / `APITimeoutError` | — | transport failures |
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from mojawave import InsufficientBalanceError, RateLimitError
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
client.sms.send(to="+255712345678", message="hi")
|
|
140
|
+
except InsufficientBalanceError:
|
|
141
|
+
... # top up
|
|
142
|
+
except RateLimitError as e:
|
|
143
|
+
time.sleep(e.retry_after or 1)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The client automatically retries `429` and `5xx` responses with exponential
|
|
147
|
+
backoff (honouring `Retry-After`), controlled by `max_retries` (default 2).
|
|
148
|
+
|
|
149
|
+
## Configuration
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
client = MojaWave(
|
|
153
|
+
api_key="sk_live_mw_...",
|
|
154
|
+
environment="live", # or "sandbox"
|
|
155
|
+
timeout=30.0, # seconds
|
|
156
|
+
max_retries=2,
|
|
157
|
+
base_url="https://api.mojawave.com/v1",
|
|
158
|
+
)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Rate-limit headers from the most recent response are available on
|
|
162
|
+
`client.rate_limit` (`.limit`, `.remaining`, `.reset`). The client is usable as
|
|
163
|
+
a context manager to ensure the HTTP session is closed:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
with MojaWave() as client:
|
|
167
|
+
client.sms.send(to="+255712345678", message="hi")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Development
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
pip install -e ".[dev]"
|
|
174
|
+
pytest
|
|
175
|
+
mypy
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Receive and verify MojaWave webhooks with Flask.
|
|
2
|
+
|
|
3
|
+
pip install flask
|
|
4
|
+
MOJAWAVE_WEBHOOK_SECRET=whsec_... python examples/flask_webhook.py
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from flask import Flask, request
|
|
10
|
+
|
|
11
|
+
from mojawave import SIGNATURE_HEADER, WebhookVerificationError, construct_event
|
|
12
|
+
|
|
13
|
+
app = Flask(__name__)
|
|
14
|
+
WEBHOOK_SECRET = os.environ["MOJAWAVE_WEBHOOK_SECRET"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.post("/webhooks/mojawave")
|
|
18
|
+
def handle_webhook():
|
|
19
|
+
signature = request.headers.get(SIGNATURE_HEADER)
|
|
20
|
+
try:
|
|
21
|
+
# Verify against the RAW body — never the parsed JSON.
|
|
22
|
+
event = construct_event(request.get_data(), signature, WEBHOOK_SECRET)
|
|
23
|
+
except WebhookVerificationError:
|
|
24
|
+
return "Forbidden", 403
|
|
25
|
+
|
|
26
|
+
if event.type == "message.delivered":
|
|
27
|
+
print(f"Delivered: {event.data['id']} -> {event.data['to']}")
|
|
28
|
+
elif event.type == "message.failed":
|
|
29
|
+
print(f"Failed: {event.data['id']} ({event.data.get('failure_reason')})")
|
|
30
|
+
elif event.type == "credits.low":
|
|
31
|
+
print("Credit balance is low — top up soon.")
|
|
32
|
+
|
|
33
|
+
return "", 204
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
app.run(port=4242)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Minimal end-to-end example. Run with MOJAWAVE_API_KEY set in your env."""
|
|
2
|
+
|
|
3
|
+
from mojawave import MojaWave, InsufficientBalanceError
|
|
4
|
+
|
|
5
|
+
client = MojaWave() # reads MOJAWAVE_API_KEY
|
|
6
|
+
|
|
7
|
+
# Check balance first.
|
|
8
|
+
balances = client.credits.balance()
|
|
9
|
+
print(f"SMS credits remaining: {balances.sms.balance}")
|
|
10
|
+
|
|
11
|
+
# Send a single message.
|
|
12
|
+
try:
|
|
13
|
+
msg = client.sms.send(
|
|
14
|
+
to="+255753276939",
|
|
15
|
+
sender="MojaWave",
|
|
16
|
+
message="Hello from Mojawave! Your code is 1234.",
|
|
17
|
+
metadata={"customer_id": "cust98765"},
|
|
18
|
+
)
|
|
19
|
+
print(f"Sent {msg.id} — status={msg.status}, cost={msg.credits_cost}")
|
|
20
|
+
except InsufficientBalanceError:
|
|
21
|
+
print("Top up your account to send messages.")
|
|
22
|
+
|
|
23
|
+
# Bulk send with per-recipient personalization.
|
|
24
|
+
job = client.sms.bulk(
|
|
25
|
+
name="Marketing Campaign Q1",
|
|
26
|
+
sender="MojaWave",
|
|
27
|
+
message="Hello {name}, your code is {code}",
|
|
28
|
+
recipients=[
|
|
29
|
+
{"to": "+255712345678", "personalization": {"name": "John", "code": "ABC123"}},
|
|
30
|
+
{"to": "+255712345679", "personalization": {"name": "Jane", "code": "XYZ789"}},
|
|
31
|
+
],
|
|
32
|
+
)
|
|
33
|
+
print(f"Bulk job {job.id}: {job.status} ({job.total_recipients} recipients)")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mojawave"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Official Python SDK for the MojaWave API — SMS and transactional messaging for Tanzania."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "MojaWave" }]
|
|
13
|
+
keywords = ["mojawave", "sms", "tanzania", "otp", "messaging", "api", "sdk"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.8",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = ["requests>=2.25"]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = ["pytest>=7.0", "responses>=0.23", "mypy>=1.0", "types-requests"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://mojawave.com"
|
|
34
|
+
Documentation = "https://mojawave.com/docs"
|
|
35
|
+
Source = "https://github.com/mojawave/mojawave-python"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/mojawave"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
42
|
+
|
|
43
|
+
[tool.mypy]
|
|
44
|
+
python_version = "3.10"
|
|
45
|
+
strict = true
|
|
46
|
+
files = ["src/mojawave"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""MojaWave Python SDK.
|
|
2
|
+
|
|
3
|
+
A thin, typed client for the MojaWave REST API — send SMS (single, bulk, OTP),
|
|
4
|
+
check credit balances, and verify webhooks.
|
|
5
|
+
|
|
6
|
+
from mojawave import MojaWave
|
|
7
|
+
|
|
8
|
+
client = MojaWave(api_key="sk_live_mw_...")
|
|
9
|
+
client.sms.send(to="+255712345678", message="Hello!")
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
__version__ = "1.0.0"
|
|
15
|
+
|
|
16
|
+
from .client import MojaWave
|
|
17
|
+
from .errors import (
|
|
18
|
+
APIConnectionError,
|
|
19
|
+
APITimeoutError,
|
|
20
|
+
AuthenticationError,
|
|
21
|
+
InsufficientBalanceError,
|
|
22
|
+
InvalidRequestError,
|
|
23
|
+
MojaWaveError,
|
|
24
|
+
RateLimitError,
|
|
25
|
+
ServerError,
|
|
26
|
+
UnprocessableError,
|
|
27
|
+
)
|
|
28
|
+
from .models import (
|
|
29
|
+
BulkJob,
|
|
30
|
+
CreditBalances,
|
|
31
|
+
Message,
|
|
32
|
+
ServiceCredits,
|
|
33
|
+
Timeline,
|
|
34
|
+
)
|
|
35
|
+
from .webhooks import (
|
|
36
|
+
SIGNATURE_HEADER,
|
|
37
|
+
WebhookEvent,
|
|
38
|
+
WebhookVerificationError,
|
|
39
|
+
compute_signature,
|
|
40
|
+
construct_event,
|
|
41
|
+
verify_signature,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"__version__",
|
|
46
|
+
"MojaWave",
|
|
47
|
+
# errors
|
|
48
|
+
"MojaWaveError",
|
|
49
|
+
"APIConnectionError",
|
|
50
|
+
"APITimeoutError",
|
|
51
|
+
"AuthenticationError",
|
|
52
|
+
"InsufficientBalanceError",
|
|
53
|
+
"InvalidRequestError",
|
|
54
|
+
"RateLimitError",
|
|
55
|
+
"ServerError",
|
|
56
|
+
"UnprocessableError",
|
|
57
|
+
# models
|
|
58
|
+
"BulkJob",
|
|
59
|
+
"CreditBalances",
|
|
60
|
+
"Message",
|
|
61
|
+
"ServiceCredits",
|
|
62
|
+
"Timeline",
|
|
63
|
+
# webhooks
|
|
64
|
+
"SIGNATURE_HEADER",
|
|
65
|
+
"WebhookEvent",
|
|
66
|
+
"WebhookVerificationError",
|
|
67
|
+
"compute_signature",
|
|
68
|
+
"construct_event",
|
|
69
|
+
"verify_signature",
|
|
70
|
+
]
|