bzapper 0.2.0__tar.gz → 0.3.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.
- {bzapper-0.2.0 → bzapper-0.3.0}/.gitignore +8 -0
- {bzapper-0.2.0 → bzapper-0.3.0}/PKG-INFO +45 -1
- {bzapper-0.2.0 → bzapper-0.3.0}/README.md +44 -0
- bzapper-0.3.0/bzapper/__init__.py +28 -0
- {bzapper-0.2.0 → bzapper-0.3.0}/bzapper/client.py +59 -0
- bzapper-0.3.0/bzapper/webhooks.py +205 -0
- {bzapper-0.2.0 → bzapper-0.3.0}/pyproject.toml +1 -1
- bzapper-0.2.0/bzapper/__init__.py +0 -13
- {bzapper-0.2.0 → bzapper-0.3.0}/.github/workflows/publish.yml +0 -0
- {bzapper-0.2.0 → bzapper-0.3.0}/LICENSE +0 -0
- {bzapper-0.2.0 → bzapper-0.3.0}/bzapper/errors.py +0 -0
- {bzapper-0.2.0 → bzapper-0.3.0}/bzapper/py.typed +0 -0
- {bzapper-0.2.0 → bzapper-0.3.0}/examples/quickstart.py +0 -0
|
@@ -44,3 +44,11 @@ apps/*/static/site.webmanifest
|
|
|
44
44
|
coverage/
|
|
45
45
|
.do/.secrets-prod.local
|
|
46
46
|
*.local
|
|
47
|
+
|
|
48
|
+
# lockfile gerado ao publicar o SDK node (não versionar)
|
|
49
|
+
clients/node/package-lock.json
|
|
50
|
+
clients/java/target/
|
|
51
|
+
__pycache__/
|
|
52
|
+
*.pyc
|
|
53
|
+
clients/python/dist/
|
|
54
|
+
clients/node/dist/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bzapper
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Official Python SDK for the bZapper WhatsApp gateway API.
|
|
5
5
|
Project-URL: Homepage, https://bzapper.com.br
|
|
6
6
|
Project-URL: Repository, https://github.com/bernisoftware/bzapper
|
|
@@ -212,6 +212,50 @@ client.get_usage() # whole period
|
|
|
212
212
|
client.get_usage(from_="2026-06-01T00:00:00Z", to="2026-06-30T23:59:59Z") # RFC3339
|
|
213
213
|
```
|
|
214
214
|
|
|
215
|
+
## Webhooks
|
|
216
|
+
|
|
217
|
+
**Manage** your webhook subscriptions:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
hook = client.create_webhook(
|
|
221
|
+
"https://yourapp.com/webhooks/bzapper",
|
|
222
|
+
event_types=["message.received", "instance.banned"], # omit = all events
|
|
223
|
+
)
|
|
224
|
+
print(hook["secret"]) # signing secret — returned ONCE, store it now
|
|
225
|
+
client.list_webhooks()
|
|
226
|
+
client.update_webhook(hook["id"], active=False) # pause
|
|
227
|
+
client.update_webhook(hook["id"], secret="regenerate") # rotate secret
|
|
228
|
+
client.delete_webhook(hook["id"])
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Receive and process** deliveries — `bzapper.webhooks` verifies the HMAC
|
|
232
|
+
signature, parses the envelope into a typed event, and routes it to your
|
|
233
|
+
handlers:
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from bzapper.webhooks import Webhooks
|
|
237
|
+
|
|
238
|
+
hooks = Webhooks(secret="whsec_...") # the secret from create_webhook
|
|
239
|
+
|
|
240
|
+
@hooks.on("message.received")
|
|
241
|
+
def _(event):
|
|
242
|
+
print(event.sender.name, event.payload.get("body"))
|
|
243
|
+
|
|
244
|
+
@hooks.on("instance.banned")
|
|
245
|
+
def _(event):
|
|
246
|
+
alert(event.instance_id)
|
|
247
|
+
|
|
248
|
+
# In your HTTP endpoint (framework-agnostic). Pass the RAW body bytes and the
|
|
249
|
+
# X-Bzapper-Signature header. Raises SignatureError if the signature is invalid.
|
|
250
|
+
hooks.handle(raw_body=request.get_data(), signature=request.headers["X-Bzapper-Signature"])
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The typed `event` has `id`, `type`, `timestamp`, `instance_id`,
|
|
254
|
+
`client_reference`, `group`, `sender`, `mentions`, `payload` and the original
|
|
255
|
+
`raw` dict. Use `event.id` for idempotency (the API may retry deliveries).
|
|
256
|
+
For lower-level use there's `verify_webhook(secret, body, signature)` and
|
|
257
|
+
`construct_webhook_event(secret, body, signature)`.
|
|
258
|
+
|
|
215
259
|
## Error handling
|
|
216
260
|
|
|
217
261
|
Non-2xx responses raise `BzapperError` with a **stable `code`**, a localized
|
|
@@ -187,6 +187,50 @@ client.get_usage() # whole period
|
|
|
187
187
|
client.get_usage(from_="2026-06-01T00:00:00Z", to="2026-06-30T23:59:59Z") # RFC3339
|
|
188
188
|
```
|
|
189
189
|
|
|
190
|
+
## Webhooks
|
|
191
|
+
|
|
192
|
+
**Manage** your webhook subscriptions:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
hook = client.create_webhook(
|
|
196
|
+
"https://yourapp.com/webhooks/bzapper",
|
|
197
|
+
event_types=["message.received", "instance.banned"], # omit = all events
|
|
198
|
+
)
|
|
199
|
+
print(hook["secret"]) # signing secret — returned ONCE, store it now
|
|
200
|
+
client.list_webhooks()
|
|
201
|
+
client.update_webhook(hook["id"], active=False) # pause
|
|
202
|
+
client.update_webhook(hook["id"], secret="regenerate") # rotate secret
|
|
203
|
+
client.delete_webhook(hook["id"])
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Receive and process** deliveries — `bzapper.webhooks` verifies the HMAC
|
|
207
|
+
signature, parses the envelope into a typed event, and routes it to your
|
|
208
|
+
handlers:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
from bzapper.webhooks import Webhooks
|
|
212
|
+
|
|
213
|
+
hooks = Webhooks(secret="whsec_...") # the secret from create_webhook
|
|
214
|
+
|
|
215
|
+
@hooks.on("message.received")
|
|
216
|
+
def _(event):
|
|
217
|
+
print(event.sender.name, event.payload.get("body"))
|
|
218
|
+
|
|
219
|
+
@hooks.on("instance.banned")
|
|
220
|
+
def _(event):
|
|
221
|
+
alert(event.instance_id)
|
|
222
|
+
|
|
223
|
+
# In your HTTP endpoint (framework-agnostic). Pass the RAW body bytes and the
|
|
224
|
+
# X-Bzapper-Signature header. Raises SignatureError if the signature is invalid.
|
|
225
|
+
hooks.handle(raw_body=request.get_data(), signature=request.headers["X-Bzapper-Signature"])
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The typed `event` has `id`, `type`, `timestamp`, `instance_id`,
|
|
229
|
+
`client_reference`, `group`, `sender`, `mentions`, `payload` and the original
|
|
230
|
+
`raw` dict. Use `event.id` for idempotency (the API may retry deliveries).
|
|
231
|
+
For lower-level use there's `verify_webhook(secret, body, signature)` and
|
|
232
|
+
`construct_webhook_event(secret, body, signature)`.
|
|
233
|
+
|
|
190
234
|
## Error handling
|
|
191
235
|
|
|
192
236
|
Non-2xx responses raise `BzapperError` with a **stable `code`**, a localized
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""bZapper — official Python SDK for the bZapper WhatsApp gateway API.
|
|
2
|
+
|
|
3
|
+
Quickstart:
|
|
4
|
+
>>> from bzapper import Client
|
|
5
|
+
>>> client = Client("http://localhost:8080", "bz_live_...")
|
|
6
|
+
>>> client.send_text("+5511999999999", "Hello from bZapper!")
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .client import Client
|
|
10
|
+
from .errors import BzapperError
|
|
11
|
+
from .webhooks import (
|
|
12
|
+
Webhooks,
|
|
13
|
+
WebhookEvent,
|
|
14
|
+
SignatureError,
|
|
15
|
+
verify as verify_webhook,
|
|
16
|
+
construct_event as construct_webhook_event,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Client",
|
|
21
|
+
"BzapperError",
|
|
22
|
+
"Webhooks",
|
|
23
|
+
"WebhookEvent",
|
|
24
|
+
"SignatureError",
|
|
25
|
+
"verify_webhook",
|
|
26
|
+
"construct_webhook_event",
|
|
27
|
+
]
|
|
28
|
+
__version__ = "0.3.0"
|
|
@@ -646,6 +646,65 @@ class Client:
|
|
|
646
646
|
"""Revoke a tenant API key by ID."""
|
|
647
647
|
return self._request("DELETE", f"/keys/{key_id}")
|
|
648
648
|
|
|
649
|
+
# -- webhooks (management; to RECEIVE+process events use bzapper.webhooks) --
|
|
650
|
+
|
|
651
|
+
def list_webhooks(self) -> JSONDict:
|
|
652
|
+
"""List the project's webhooks. ``GET /webhooks``"""
|
|
653
|
+
return self._request("GET", "/webhooks")
|
|
654
|
+
|
|
655
|
+
def create_webhook(
|
|
656
|
+
self,
|
|
657
|
+
url: str,
|
|
658
|
+
*,
|
|
659
|
+
secret: Optional[str] = None,
|
|
660
|
+
event_types: Optional[Sequence[str]] = None,
|
|
661
|
+
number_filter: Optional[str] = None,
|
|
662
|
+
) -> JSONDict:
|
|
663
|
+
"""Create a webhook. ``POST /webhooks``
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
url: HTTPS endpoint that will receive the deliveries.
|
|
667
|
+
secret: Omit to let the API generate a strong one (returned ONCE in
|
|
668
|
+
``secret``). Use it with :class:`bzapper.webhooks.Webhooks`.
|
|
669
|
+
event_types: Subscribed events; empty/None = all. Each event can
|
|
670
|
+
belong to a single webhook (409 on conflict).
|
|
671
|
+
number_filter: ``instance_id`` to restrict to one number.
|
|
672
|
+
"""
|
|
673
|
+
return self._request(
|
|
674
|
+
"POST",
|
|
675
|
+
"/webhooks",
|
|
676
|
+
body={"url": url, "secret": secret, "event_types": event_types, "number_filter": number_filter},
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
def update_webhook(
|
|
680
|
+
self,
|
|
681
|
+
webhook_id: str,
|
|
682
|
+
*,
|
|
683
|
+
url: Optional[str] = None,
|
|
684
|
+
secret: Optional[str] = None,
|
|
685
|
+
event_types: Optional[Sequence[str]] = None,
|
|
686
|
+
number_filter: Optional[str] = None,
|
|
687
|
+
active: Optional[bool] = None,
|
|
688
|
+
) -> JSONDict:
|
|
689
|
+
"""Update/pause a webhook. ``secret="regenerate"`` rotates it. ``PATCH /webhooks/{id}``"""
|
|
690
|
+
return self._request(
|
|
691
|
+
"PATCH",
|
|
692
|
+
f"/webhooks/{webhook_id}",
|
|
693
|
+
body={"url": url, "secret": secret, "event_types": event_types, "number_filter": number_filter, "active": active},
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
def delete_webhook(self, webhook_id: str) -> None:
|
|
697
|
+
"""Delete a webhook. ``DELETE /webhooks/{id}``"""
|
|
698
|
+
return self._request("DELETE", f"/webhooks/{webhook_id}")
|
|
699
|
+
|
|
700
|
+
def test_webhook(self, webhook_id: str, event_type: Optional[str] = None) -> JSONDict:
|
|
701
|
+
"""Send a test event and return the endpoint's HTTP status. ``POST /webhooks/{id}/test``"""
|
|
702
|
+
return self._request("POST", f"/webhooks/{webhook_id}/test", body={"event_type": event_type})
|
|
703
|
+
|
|
704
|
+
def webhook_deliveries(self, webhook_id: str, *, limit: Optional[int] = None) -> JSONDict:
|
|
705
|
+
"""Recent delivery attempts for a webhook. ``GET /webhooks/{id}/deliveries``"""
|
|
706
|
+
return self._request("GET", f"/webhooks/{webhook_id}/deliveries", params={"limit": limit})
|
|
707
|
+
|
|
649
708
|
# -- usage -----------------------------------------------------------------
|
|
650
709
|
|
|
651
710
|
def get_usage(
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Webhook receiver for bZapper.
|
|
2
|
+
|
|
3
|
+
Receives the raw request body, **verifies the HMAC-SHA256 signature**, parses the
|
|
4
|
+
envelope into a typed :class:`WebhookEvent` and routes it to handlers registered
|
|
5
|
+
per event type. Zero third-party dependencies (stdlib only).
|
|
6
|
+
|
|
7
|
+
The API signs every delivery with ``X-Bzapper-Signature: sha256=<hex>`` where the
|
|
8
|
+
hex is ``HMAC_SHA256(secret, raw_body)``. It also sends ``X-Bzapper-Event-Id`` and
|
|
9
|
+
``X-Bzapper-Event-Type``.
|
|
10
|
+
|
|
11
|
+
Quickstart::
|
|
12
|
+
|
|
13
|
+
from bzapper.webhooks import Webhooks
|
|
14
|
+
|
|
15
|
+
hooks = Webhooks(secret="whsec_...") # the webhook's secret (from create_webhook)
|
|
16
|
+
|
|
17
|
+
@hooks.on("message.received")
|
|
18
|
+
def _(event):
|
|
19
|
+
print(event.sender.name, event.payload.get("body"))
|
|
20
|
+
|
|
21
|
+
# In your HTTP endpoint (Flask/FastAPI/Django — framework-agnostic):
|
|
22
|
+
# verifies the signature, parses, and dispatches. Raises SignatureError if bad.
|
|
23
|
+
hooks.handle(raw_body=request.get_data(), signature=request.headers["X-Bzapper-Signature"])
|
|
24
|
+
|
|
25
|
+
Idempotency: each event carries a stable ``event.id`` — store processed ids
|
|
26
|
+
(Redis/DB) and skip duplicates; the API may retry deliveries.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import hashlib
|
|
32
|
+
import hmac
|
|
33
|
+
import json
|
|
34
|
+
from dataclasses import dataclass, field
|
|
35
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"Webhooks",
|
|
39
|
+
"WebhookEvent",
|
|
40
|
+
"Group",
|
|
41
|
+
"Sender",
|
|
42
|
+
"SignatureError",
|
|
43
|
+
"verify",
|
|
44
|
+
"construct_event",
|
|
45
|
+
"SIGNATURE_HEADER",
|
|
46
|
+
"EVENT_ID_HEADER",
|
|
47
|
+
"EVENT_TYPE_HEADER",
|
|
48
|
+
"EVENT_TYPES",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
SIGNATURE_HEADER = "X-Bzapper-Signature"
|
|
52
|
+
EVENT_ID_HEADER = "X-Bzapper-Event-Id"
|
|
53
|
+
EVENT_TYPE_HEADER = "X-Bzapper-Event-Type"
|
|
54
|
+
|
|
55
|
+
#: All event types the API can deliver (for reference/autocomplete).
|
|
56
|
+
EVENT_TYPES = (
|
|
57
|
+
"message.received", "message.sent", "message.delivered", "message.read", "message.failed",
|
|
58
|
+
"instance.connected", "instance.disconnected", "instance.banned", "instance.logged_out",
|
|
59
|
+
"instance.warming", "instance.status",
|
|
60
|
+
"group.joined", "group.participant_added", "group.participant_removed",
|
|
61
|
+
"group.participant_promoted", "group.participant_demoted",
|
|
62
|
+
"group.subject_changed", "group.description_changed",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
Body = Union[str, bytes, bytearray]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SignatureError(Exception):
|
|
69
|
+
"""Raised when a webhook signature is missing or does not match."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class Group:
|
|
74
|
+
"""WhatsApp group context, when the event happened in a group."""
|
|
75
|
+
|
|
76
|
+
jid: Optional[str] = None
|
|
77
|
+
name: Optional[str] = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class Sender:
|
|
82
|
+
"""Who sent/triggered the event (for message/group events)."""
|
|
83
|
+
|
|
84
|
+
jid: Optional[str] = None
|
|
85
|
+
lid: Optional[str] = None
|
|
86
|
+
name: Optional[str] = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class WebhookEvent:
|
|
91
|
+
"""A parsed, typed webhook event (the delivered envelope)."""
|
|
92
|
+
|
|
93
|
+
id: str
|
|
94
|
+
type: str
|
|
95
|
+
timestamp: Optional[str] = None
|
|
96
|
+
instance_id: Optional[str] = None
|
|
97
|
+
client_reference: Optional[str] = None
|
|
98
|
+
group: Optional[Group] = None
|
|
99
|
+
sender: Optional[Sender] = None
|
|
100
|
+
mentions: List[str] = field(default_factory=list)
|
|
101
|
+
payload: Dict[str, Any] = field(default_factory=dict)
|
|
102
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def from_dict(cls, d: Dict[str, Any]) -> "WebhookEvent":
|
|
106
|
+
g = d.get("group")
|
|
107
|
+
s = d.get("sender")
|
|
108
|
+
return cls(
|
|
109
|
+
id=d.get("event_id", ""),
|
|
110
|
+
type=d.get("event_type", ""),
|
|
111
|
+
timestamp=d.get("timestamp"),
|
|
112
|
+
instance_id=d.get("instance_id"),
|
|
113
|
+
client_reference=d.get("client_reference"),
|
|
114
|
+
group=Group(jid=g.get("jid"), name=g.get("name")) if isinstance(g, dict) else None,
|
|
115
|
+
sender=Sender(jid=s.get("jid"), lid=s.get("lid"), name=s.get("name")) if isinstance(s, dict) else None,
|
|
116
|
+
mentions=list(d.get("mentions") or []),
|
|
117
|
+
payload=dict(d.get("payload") or {}),
|
|
118
|
+
raw=d,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _as_bytes(body: Body) -> bytes:
|
|
123
|
+
return body.encode("utf-8") if isinstance(body, str) else bytes(body)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def verify(secret: str, body: Body, signature: Optional[str]) -> bool:
|
|
127
|
+
"""Return True iff ``signature`` matches the HMAC of the **raw** body.
|
|
128
|
+
|
|
129
|
+
Timing-safe. Pass the exact bytes received — never the re-serialized JSON.
|
|
130
|
+
"""
|
|
131
|
+
if not signature:
|
|
132
|
+
return False
|
|
133
|
+
expected = "sha256=" + hmac.new(secret.encode("utf-8"), _as_bytes(body), hashlib.sha256).hexdigest()
|
|
134
|
+
return hmac.compare_digest(expected, signature)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def construct_event(secret: str, body: Body, signature: Optional[str]) -> WebhookEvent:
|
|
138
|
+
"""Verify the signature and parse the body into a :class:`WebhookEvent`.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
SignatureError: if the signature is missing or invalid.
|
|
142
|
+
"""
|
|
143
|
+
if not verify(secret, body, signature):
|
|
144
|
+
raise SignatureError("invalid webhook signature")
|
|
145
|
+
text = body.decode("utf-8") if isinstance(body, (bytes, bytearray)) else body
|
|
146
|
+
return WebhookEvent.from_dict(json.loads(text))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
Handler = Callable[[WebhookEvent], None]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class Webhooks:
|
|
153
|
+
"""Verifies, parses and routes incoming webhook deliveries to handlers.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
secret: The webhook's signing secret (returned once by ``create_webhook``).
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def __init__(self, secret: str) -> None:
|
|
160
|
+
if not secret:
|
|
161
|
+
raise ValueError("Webhooks: `secret` is required.")
|
|
162
|
+
self.secret = secret
|
|
163
|
+
self._handlers: Dict[str, List[Handler]] = {}
|
|
164
|
+
self._any: List[Handler] = []
|
|
165
|
+
|
|
166
|
+
def on(self, event_type: str, handler: Optional[Handler] = None):
|
|
167
|
+
"""Register a handler for an event type. Usable as a decorator.
|
|
168
|
+
|
|
169
|
+
::
|
|
170
|
+
|
|
171
|
+
@hooks.on("message.received")
|
|
172
|
+
def _(event): ...
|
|
173
|
+
|
|
174
|
+
hooks.on("instance.banned", my_handler)
|
|
175
|
+
"""
|
|
176
|
+
def register(h: Handler) -> Handler:
|
|
177
|
+
self._handlers.setdefault(event_type, []).append(h)
|
|
178
|
+
return h
|
|
179
|
+
|
|
180
|
+
return register(handler) if handler is not None else register
|
|
181
|
+
|
|
182
|
+
def on_any(self, handler: Optional[Handler] = None):
|
|
183
|
+
"""Register a handler that runs for **every** event. Usable as a decorator."""
|
|
184
|
+
def register(h: Handler) -> Handler:
|
|
185
|
+
self._any.append(h)
|
|
186
|
+
return h
|
|
187
|
+
|
|
188
|
+
return register(handler) if handler is not None else register
|
|
189
|
+
|
|
190
|
+
def construct_event(self, body: Body, signature: Optional[str]) -> WebhookEvent:
|
|
191
|
+
"""Verify + parse a delivery into a typed event (no dispatch)."""
|
|
192
|
+
return construct_event(self.secret, body, signature)
|
|
193
|
+
|
|
194
|
+
def handle(self, raw_body: Body, signature: Optional[str]) -> WebhookEvent:
|
|
195
|
+
"""Verify, parse and dispatch a delivery to the matching handlers.
|
|
196
|
+
|
|
197
|
+
Returns the parsed event (use ``event.id`` for idempotency). Raises
|
|
198
|
+
:class:`SignatureError` if the signature is invalid — do NOT process.
|
|
199
|
+
"""
|
|
200
|
+
event = self.construct_event(raw_body, signature)
|
|
201
|
+
for h in self._handlers.get(event.type, []):
|
|
202
|
+
h(event)
|
|
203
|
+
for h in self._any:
|
|
204
|
+
h(event)
|
|
205
|
+
return event
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
"""bZapper — official Python SDK for the bZapper WhatsApp gateway API.
|
|
2
|
-
|
|
3
|
-
Quickstart:
|
|
4
|
-
>>> from bzapper import Client
|
|
5
|
-
>>> client = Client("http://localhost:8080", "bz_live_...")
|
|
6
|
-
>>> client.send_text("+5511999999999", "Hello from bZapper!")
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from .client import Client
|
|
10
|
-
from .errors import BzapperError
|
|
11
|
-
|
|
12
|
-
__all__ = ["Client", "BzapperError"]
|
|
13
|
-
__version__ = "0.2.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|