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/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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any