wazzapi 0.1.0__py3-none-any.whl
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.
- wazzapi/__init__.py +36 -0
- wazzapi/client.py +120 -0
- wazzapi/errors.py +60 -0
- wazzapi/models/__init__.py +11 -0
- wazzapi/models/base.py +10 -0
- wazzapi/models/contacts.py +204 -0
- wazzapi/models/messages.py +173 -0
- wazzapi/models/templates.py +100 -0
- wazzapi/models/webhooks.py +87 -0
- wazzapi/py.typed +0 -0
- wazzapi/resources/__init__.py +5 -0
- wazzapi/resources/base.py +14 -0
- wazzapi/resources/contacts.py +222 -0
- wazzapi/resources/messages.py +174 -0
- wazzapi/resources/templates.py +87 -0
- wazzapi/webhooks.py +131 -0
- wazzapi-0.1.0.dist-info/METADATA +201 -0
- wazzapi-0.1.0.dist-info/RECORD +19 -0
- wazzapi-0.1.0.dist-info/WHEEL +4 -0
wazzapi/webhooks.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .models.webhooks import PublicDeviceWebhook, PublicMessageWebhook, WebhookPayload
|
|
10
|
+
|
|
11
|
+
SIGNATURE_HEADER = "X-Wazzapi-Signature"
|
|
12
|
+
EVENT_HEADER = "X-Wazzapi-Event"
|
|
13
|
+
EVENT_ID_HEADER = "X-Wazzapi-Event-ID"
|
|
14
|
+
_SIGNATURE_PREFIX = "sha256="
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WazzapiWebhookError(Exception):
|
|
18
|
+
"""Base exception for webhook helpers."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WazzapiWebhookVerificationError(WazzapiWebhookError):
|
|
22
|
+
"""Raised when webhook signature verification fails."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class WazzapiWebhookParseError(WazzapiWebhookError):
|
|
26
|
+
"""Raised when a webhook payload cannot be parsed."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def generate_webhook_signature(payload: bytes | str, secret: str) -> str:
|
|
30
|
+
payload_bytes = _to_bytes(payload)
|
|
31
|
+
digest = hmac.new(secret.encode("utf-8"), payload_bytes, hashlib.sha256).hexdigest()
|
|
32
|
+
return f"{_SIGNATURE_PREFIX}{digest}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def verify_webhook_signature(payload: bytes | str, signature: str, secret: str) -> bool:
|
|
36
|
+
normalized_signature = signature.strip()
|
|
37
|
+
expected = generate_webhook_signature(payload, secret)
|
|
38
|
+
return hmac.compare_digest(expected, normalized_signature)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class WebhookHandler:
|
|
42
|
+
"""Verify and parse incoming WazzAPI webhook deliveries."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, secret: str) -> None:
|
|
45
|
+
self.secret = secret
|
|
46
|
+
|
|
47
|
+
def generate_signature(self, payload: bytes | str) -> str:
|
|
48
|
+
return generate_webhook_signature(payload, self.secret)
|
|
49
|
+
|
|
50
|
+
def verify_signature(self, payload: bytes | str, signature: str) -> bool:
|
|
51
|
+
return verify_webhook_signature(payload, signature, self.secret)
|
|
52
|
+
|
|
53
|
+
def verify_headers(self, payload: bytes | str, headers: Mapping[str, str]) -> None:
|
|
54
|
+
signature = _get_header(headers, SIGNATURE_HEADER)
|
|
55
|
+
if not signature:
|
|
56
|
+
raise WazzapiWebhookVerificationError(
|
|
57
|
+
f"Missing required webhook header: {SIGNATURE_HEADER}"
|
|
58
|
+
)
|
|
59
|
+
if not self.verify_signature(payload, signature):
|
|
60
|
+
raise WazzapiWebhookVerificationError("Invalid webhook signature")
|
|
61
|
+
|
|
62
|
+
if not _get_header(headers, EVENT_HEADER):
|
|
63
|
+
raise WazzapiWebhookVerificationError(
|
|
64
|
+
f"Missing required webhook header: {EVENT_HEADER}"
|
|
65
|
+
)
|
|
66
|
+
if not _get_header(headers, EVENT_ID_HEADER):
|
|
67
|
+
raise WazzapiWebhookVerificationError(
|
|
68
|
+
f"Missing required webhook header: {EVENT_ID_HEADER}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def parse(self, payload: bytes | str) -> WebhookPayload:
|
|
72
|
+
payload_bytes = _to_bytes(payload)
|
|
73
|
+
try:
|
|
74
|
+
raw_payload = json.loads(payload_bytes.decode("utf-8"))
|
|
75
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
76
|
+
raise WazzapiWebhookParseError("Webhook payload is not valid JSON") from exc
|
|
77
|
+
|
|
78
|
+
if not isinstance(raw_payload, dict):
|
|
79
|
+
raise WazzapiWebhookParseError("Webhook payload must be a JSON object")
|
|
80
|
+
|
|
81
|
+
event_type = raw_payload.get("event_type")
|
|
82
|
+
if not isinstance(event_type, str):
|
|
83
|
+
raise WazzapiWebhookParseError("Webhook payload is missing event_type")
|
|
84
|
+
|
|
85
|
+
if event_type.startswith("message."):
|
|
86
|
+
return PublicMessageWebhook.model_validate(raw_payload)
|
|
87
|
+
if event_type.startswith("device."):
|
|
88
|
+
return PublicDeviceWebhook.model_validate(raw_payload)
|
|
89
|
+
raise WazzapiWebhookParseError(f"Unsupported webhook event type: {event_type}")
|
|
90
|
+
|
|
91
|
+
def verify_and_parse(self, payload: bytes | str, headers: Mapping[str, str]) -> WebhookPayload:
|
|
92
|
+
self.verify_headers(payload, headers)
|
|
93
|
+
parsed = self.parse(payload)
|
|
94
|
+
header_event = _get_header(headers, EVENT_HEADER)
|
|
95
|
+
if header_event and header_event != parsed.event_type:
|
|
96
|
+
raise WazzapiWebhookVerificationError(
|
|
97
|
+
"Webhook event header does not match payload event_type"
|
|
98
|
+
)
|
|
99
|
+
return parsed
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def parse_webhook(payload: bytes | str, headers: Mapping[str, str], secret: str) -> WebhookPayload:
|
|
103
|
+
return WebhookHandler(secret).verify_and_parse(payload, headers)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _to_bytes(payload: bytes | str) -> bytes:
|
|
107
|
+
if isinstance(payload, bytes):
|
|
108
|
+
return payload
|
|
109
|
+
return payload.encode("utf-8")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _get_header(headers: Mapping[str, str], name: str) -> str | None:
|
|
113
|
+
expected = name.lower()
|
|
114
|
+
for key, value in headers.items():
|
|
115
|
+
if key.lower() == expected:
|
|
116
|
+
return value
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
__all__ = [
|
|
121
|
+
"EVENT_HEADER",
|
|
122
|
+
"EVENT_ID_HEADER",
|
|
123
|
+
"SIGNATURE_HEADER",
|
|
124
|
+
"WebhookHandler",
|
|
125
|
+
"WazzapiWebhookError",
|
|
126
|
+
"WazzapiWebhookParseError",
|
|
127
|
+
"WazzapiWebhookVerificationError",
|
|
128
|
+
"generate_webhook_signature",
|
|
129
|
+
"parse_webhook",
|
|
130
|
+
"verify_webhook_signature",
|
|
131
|
+
]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wazzapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed Python SDK for WazzAPI
|
|
5
|
+
Project-URL: API, https://api.wazzapi.com
|
|
6
|
+
Keywords: api,sdk,wazzapi,webhook,whatsapp
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: httpx<1.0,>=0.28
|
|
20
|
+
Requires-Dist: pydantic<3.0,>=2.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# WazzAPI Python SDK
|
|
24
|
+
|
|
25
|
+
Official Python SDK for WazzAPI.
|
|
26
|
+
|
|
27
|
+
Use it to send messages, manage contacts and templates, and verify incoming webhooks with a simple typed client.
|
|
28
|
+
|
|
29
|
+
## What you can do with this SDK
|
|
30
|
+
|
|
31
|
+
- send direct WhatsApp messages
|
|
32
|
+
- list, create, update, and delete contacts
|
|
33
|
+
- manage message templates and preview rendered content
|
|
34
|
+
- verify and parse incoming WazzAPI webhooks
|
|
35
|
+
- work with typed request and response models
|
|
36
|
+
|
|
37
|
+
## Requirements
|
|
38
|
+
|
|
39
|
+
- Python 3.10+
|
|
40
|
+
- a WazzAPI account
|
|
41
|
+
- a WazzAPI API key
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
From PyPI:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install wazzapi-py
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
With uv:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv add wazzapi-py
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Need a WazzAPI account first?
|
|
58
|
+
|
|
59
|
+
Sign up at `https://app.wazzapi.com` using your Google organization account. On first sign-in, WazzAPI creates a new workspace for you automatically.
|
|
60
|
+
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
For most integrations, you only need your API key:
|
|
64
|
+
|
|
65
|
+
- `WAZZAPI_API_KEY`
|
|
66
|
+
|
|
67
|
+
If you plan to receive webhooks, also configure:
|
|
68
|
+
|
|
69
|
+
- `WAZZAPI_WEBHOOK_SECRET`
|
|
70
|
+
|
|
71
|
+
The SDK uses `https://api.wazzapi.com` by default, so you do not need to set a base URL.
|
|
72
|
+
|
|
73
|
+
## Quick start
|
|
74
|
+
|
|
75
|
+
### Send a message
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from wazzapi import SendMessageRequest, WazzapiClient
|
|
79
|
+
|
|
80
|
+
with WazzapiClient(api_key="your-api-key") as client:
|
|
81
|
+
response = client.messages.send(
|
|
82
|
+
SendMessageRequest(
|
|
83
|
+
phone_number="+6281234567890",
|
|
84
|
+
whatsapp_account_id="your-whatsapp-account-id",
|
|
85
|
+
content="Hello from WazzAPI!",
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
print(response.model_dump())
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Error handling
|
|
93
|
+
|
|
94
|
+
When the API returns a non-success response, the SDK raises `WazzapiAPIError`.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from wazzapi import WazzapiAPIError, WazzapiClient
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
with WazzapiClient(api_key="your-api-key") as client:
|
|
101
|
+
client.messages.get("missing-message-id")
|
|
102
|
+
except WazzapiAPIError as exc:
|
|
103
|
+
print(exc.status_code)
|
|
104
|
+
print(exc.message)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## More usage examples
|
|
108
|
+
|
|
109
|
+
### List contacts
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from wazzapi import WazzapiClient
|
|
113
|
+
|
|
114
|
+
with WazzapiClient(api_key="your-api-key") as client:
|
|
115
|
+
response = client.contacts.list(limit=20, search="alice")
|
|
116
|
+
|
|
117
|
+
for contact in response.contacts:
|
|
118
|
+
print(contact.model_dump())
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Create a template
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from wazzapi import TemplateCreateRequest, WazzapiClient
|
|
125
|
+
|
|
126
|
+
with WazzapiClient(api_key="your-api-key") as client:
|
|
127
|
+
template = client.templates.create(
|
|
128
|
+
TemplateCreateRequest(
|
|
129
|
+
name="welcome-message",
|
|
130
|
+
category="marketing",
|
|
131
|
+
content="Hi {{name}}, welcome to WazzAPI!",
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
print(template.model_dump())
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Preview a template
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from wazzapi import TemplatePreviewRequest, WazzapiClient
|
|
142
|
+
|
|
143
|
+
with WazzapiClient(api_key="your-api-key") as client:
|
|
144
|
+
preview = client.templates.preview(
|
|
145
|
+
TemplatePreviewRequest(
|
|
146
|
+
content="Hi {{name}}, your code is {{code}}.",
|
|
147
|
+
custom_variables={"name": "Alice", "code": "WZ-1234"},
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
print(preview.model_dump())
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Webhook verification
|
|
155
|
+
|
|
156
|
+
Use `WebhookHandler` to verify the raw request body against `X-Wazzapi-Signature` before parsing JSON.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from wazzapi import WebhookHandler
|
|
160
|
+
|
|
161
|
+
handler = WebhookHandler("your-webhook-secret")
|
|
162
|
+
webhook = handler.verify_and_parse(raw_body, request.headers)
|
|
163
|
+
|
|
164
|
+
print(webhook.event_type)
|
|
165
|
+
print(webhook.data.model_dump())
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
WazzAPI webhook headers:
|
|
169
|
+
|
|
170
|
+
- `X-Wazzapi-Signature`
|
|
171
|
+
- `X-Wazzapi-Event`
|
|
172
|
+
- `X-Wazzapi-Event-ID`
|
|
173
|
+
|
|
174
|
+
Supported webhook event families:
|
|
175
|
+
|
|
176
|
+
- message events: `message.received`, `message.sent`, `message.delivered`, `message.read`, `message.failed`
|
|
177
|
+
- device events: `device.connected`, `device.disconnected`
|
|
178
|
+
|
|
179
|
+
## Examples
|
|
180
|
+
|
|
181
|
+
Ready-to-run examples live in `examples/`:
|
|
182
|
+
|
|
183
|
+
- `examples/list_contacts.py`
|
|
184
|
+
- `examples/send_message.py`
|
|
185
|
+
- `examples/create_template.py`
|
|
186
|
+
- `examples/preview_template.py`
|
|
187
|
+
- `examples/verify_webhook.py`
|
|
188
|
+
|
|
189
|
+
Run them with:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
uv run python examples/list_contacts.py
|
|
193
|
+
uv run python examples/send_message.py
|
|
194
|
+
uv run python examples/create_template.py
|
|
195
|
+
uv run python examples/preview_template.py
|
|
196
|
+
uv run python examples/verify_webhook.py
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Release automation
|
|
200
|
+
|
|
201
|
+
This repository includes a GitHub Actions workflow that publishes to PyPI automatically when a tag matching `v*` is pushed.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
wazzapi/__init__.py,sha256=nChxmuPk-41f1CswitT1IDLTqq_2VUrBGYpaYK6tS_k,833
|
|
2
|
+
wazzapi/client.py,sha256=Ps-rPR2oWGaH_R2-7KrjjNOWvBUlYRXQckShx_q-8M8,3468
|
|
3
|
+
wazzapi/errors.py,sha256=RodZNLatVsw2JP5YR15TwQXlpSnkXEv4-wxqGGDosdM,1746
|
|
4
|
+
wazzapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
wazzapi/webhooks.py,sha256=U824l0_HJsRVdXyuG5CIrtlYb_HiHHdSzQkVce7Jsig,4704
|
|
6
|
+
wazzapi/models/__init__.py,sha256=vfQ970RjkqgG8Ph2YsIaLQb4EkWCabXU9-7kyTzGfS4,402
|
|
7
|
+
wazzapi/models/base.py,sha256=A8o-YUNz9wYrOQMAqyMIAcpYr5y-Tc0mZ_szY1nziB8,234
|
|
8
|
+
wazzapi/models/contacts.py,sha256=byhUrOiVW7NalQE7Y4Y9WVsRhZCInZV2VSv9ZCVUOAA,4516
|
|
9
|
+
wazzapi/models/messages.py,sha256=1oaAAJQ36fasktbmLje4uHVwMm5YYuO4VvwFxbv2ukI,4100
|
|
10
|
+
wazzapi/models/templates.py,sha256=VGZRYpYuM6fak0eYPn0fTuOS-6U80ayhcsSbaLC7YSU,2532
|
|
11
|
+
wazzapi/models/webhooks.py,sha256=qCVeNtuUNbkC--bAJEmbxHNv36MvVZ6q3SA_Yt3WWfM,2159
|
|
12
|
+
wazzapi/resources/__init__.py,sha256=Hagf7dJiXKFl08L0tphibKSFmxL50F0xHAjIzSf93qU,191
|
|
13
|
+
wazzapi/resources/base.py,sha256=4gNtuTUz5dQo3HsEbplw27F1miVaUSlV8Zqvc6minLA,265
|
|
14
|
+
wazzapi/resources/contacts.py,sha256=Oot002UJDcLiFPGd5h5EouR_76QikYnTGyj_uuEYz30,6564
|
|
15
|
+
wazzapi/resources/messages.py,sha256=Ry4djOfPKWwiZQeNUOnWlHT6HiQhrWixDJSrRT5xl_4,5332
|
|
16
|
+
wazzapi/resources/templates.py,sha256=Rlgpg9gPc1K0TbTBGzDJWdXL8uCKC1ZsBU_dfFStqTg,2518
|
|
17
|
+
wazzapi-0.1.0.dist-info/METADATA,sha256=Xha6PPptDYYtDI0JlLviD5nrXixWSqxsifxty6_8S4I,4974
|
|
18
|
+
wazzapi-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
19
|
+
wazzapi-0.1.0.dist-info/RECORD,,
|