wazzapi 0.1.2__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.
Files changed (39) hide show
  1. {wazzapi-0.1.2 → wazzapi-0.3.0}/PKG-INFO +19 -3
  2. {wazzapi-0.1.2 → wazzapi-0.3.0}/README.md +17 -1
  3. wazzapi-0.3.0/examples/list_devices.py +15 -0
  4. {wazzapi-0.1.2 → wazzapi-0.3.0}/pyproject.toml +2 -2
  5. {wazzapi-0.1.2 → wazzapi-0.3.0}/tests/smoke_test.py +1 -1
  6. {wazzapi-0.1.2 → wazzapi-0.3.0}/tests/test_client.py +1 -0
  7. wazzapi-0.3.0/tests/test_groups.py +285 -0
  8. {wazzapi-0.1.2 → wazzapi-0.3.0}/tests/test_resources.py +56 -0
  9. {wazzapi-0.1.2 → wazzapi-0.3.0}/wazzapi/__init__.py +1 -1
  10. {wazzapi-0.1.2 → wazzapi-0.3.0}/wazzapi/client.py +3 -1
  11. {wazzapi-0.1.2 → wazzapi-0.3.0}/wazzapi/models/__init__.py +14 -1
  12. wazzapi-0.3.0/wazzapi/models/devices.py +42 -0
  13. wazzapi-0.3.0/wazzapi/models/groups.py +165 -0
  14. wazzapi-0.3.0/wazzapi/resources/__init__.py +13 -0
  15. wazzapi-0.3.0/wazzapi/resources/devices.py +40 -0
  16. wazzapi-0.3.0/wazzapi/resources/groups.py +266 -0
  17. wazzapi-0.1.2/wazzapi/resources/__init__.py +0 -5
  18. {wazzapi-0.1.2 → wazzapi-0.3.0}/.gitignore +0 -0
  19. {wazzapi-0.1.2 → wazzapi-0.3.0}/examples/create_template.py +0 -0
  20. {wazzapi-0.1.2 → wazzapi-0.3.0}/examples/list_contacts.py +0 -0
  21. {wazzapi-0.1.2 → wazzapi-0.3.0}/examples/preview_template.py +0 -0
  22. {wazzapi-0.1.2 → wazzapi-0.3.0}/examples/send_message.py +0 -0
  23. {wazzapi-0.1.2 → wazzapi-0.3.0}/examples/verify_webhook.py +0 -0
  24. {wazzapi-0.1.2 → wazzapi-0.3.0}/tests/test_media.py +0 -0
  25. {wazzapi-0.1.2 → wazzapi-0.3.0}/tests/test_webhooks.py +0 -0
  26. {wazzapi-0.1.2 → wazzapi-0.3.0}/wazzapi/errors.py +0 -0
  27. {wazzapi-0.1.2 → wazzapi-0.3.0}/wazzapi/media.py +0 -0
  28. {wazzapi-0.1.2 → wazzapi-0.3.0}/wazzapi/models/base.py +0 -0
  29. {wazzapi-0.1.2 → wazzapi-0.3.0}/wazzapi/models/contacts.py +0 -0
  30. {wazzapi-0.1.2 → wazzapi-0.3.0}/wazzapi/models/media.py +0 -0
  31. {wazzapi-0.1.2 → wazzapi-0.3.0}/wazzapi/models/messages.py +0 -0
  32. {wazzapi-0.1.2 → wazzapi-0.3.0}/wazzapi/models/templates.py +0 -0
  33. {wazzapi-0.1.2 → wazzapi-0.3.0}/wazzapi/models/webhooks.py +0 -0
  34. {wazzapi-0.1.2 → wazzapi-0.3.0}/wazzapi/py.typed +0 -0
  35. {wazzapi-0.1.2 → wazzapi-0.3.0}/wazzapi/resources/base.py +0 -0
  36. {wazzapi-0.1.2 → wazzapi-0.3.0}/wazzapi/resources/contacts.py +0 -0
  37. {wazzapi-0.1.2 → wazzapi-0.3.0}/wazzapi/resources/messages.py +0 -0
  38. {wazzapi-0.1.2 → wazzapi-0.3.0}/wazzapi/resources/templates.py +0 -0
  39. {wazzapi-0.1.2 → wazzapi-0.3.0}/wazzapi/webhooks.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wazzapi
3
- Version: 0.1.2
3
+ Version: 0.3.0
4
4
  Summary: Typed Python SDK for WazzAPI
5
- Project-URL: Homepage, https://github.com/wazzapihq/wazzapi-py
6
5
  Project-URL: API, https://api.wazzapi.com
6
+ Project-URL: Homepage, https://github.com/wazzapihq/wazzapi-py
7
7
  Keywords: api,sdk,wazzapi,webhook,whatsapp
8
8
  Classifier: Development Status :: 4 - Beta
9
9
  Classifier: Intended Audience :: Developers
@@ -26,11 +26,12 @@ Description-Content-Type: text/markdown
26
26
 
27
27
  Official Python SDK for WazzAPI.
28
28
 
29
- Use it to send messages, manage contacts and templates, and verify incoming webhooks with a simple typed client.
29
+ Use it to send messages, inspect WhatsApp devices, manage contacts and templates, and verify incoming webhooks with a simple typed client.
30
30
 
31
31
  ## What you can do with this SDK
32
32
 
33
33
  - send direct WhatsApp messages
34
+ - list and inspect WhatsApp devices
34
35
  - list, create, update, and delete contacts
35
36
  - manage message templates and preview rendered content
36
37
  - verify and parse incoming WazzAPI webhooks
@@ -118,6 +119,19 @@ with WazzapiClient(api_key="your-api-key") as client:
118
119
 
119
120
  for contact in response.contacts:
120
121
  print(contact.model_dump())
122
+
123
+ ### List devices
124
+
125
+ ```python
126
+ from wazzapi import WazzapiClient
127
+
128
+ with WazzapiClient(api_key="your-api-key") as client:
129
+ response = client.devices.list(limit=20, status="connected")
130
+
131
+ for device in response.devices:
132
+ print(device.name, device.session_name, device.status)
133
+ ```
134
+
121
135
  ```
122
136
 
123
137
  ### Create a template
@@ -182,6 +196,7 @@ Supported webhook event families:
182
196
 
183
197
  Ready-to-run examples live in `examples/`:
184
198
 
199
+ - `examples/list_devices.py`
185
200
  - `examples/list_contacts.py`
186
201
  - `examples/send_message.py`
187
202
  - `examples/create_template.py`
@@ -192,6 +207,7 @@ Run them with:
192
207
 
193
208
  ```bash
194
209
  uv run python examples/list_contacts.py
210
+ uv run python examples/list_devices.py
195
211
  uv run python examples/send_message.py
196
212
  uv run python examples/create_template.py
197
213
  uv run python examples/preview_template.py
@@ -2,11 +2,12 @@
2
2
 
3
3
  Official Python SDK for WazzAPI.
4
4
 
5
- Use it to send messages, manage contacts and templates, and verify incoming webhooks with a simple typed client.
5
+ Use it to send messages, inspect WhatsApp devices, manage contacts and templates, and verify incoming webhooks with a simple typed client.
6
6
 
7
7
  ## What you can do with this SDK
8
8
 
9
9
  - send direct WhatsApp messages
10
+ - list and inspect WhatsApp devices
10
11
  - list, create, update, and delete contacts
11
12
  - manage message templates and preview rendered content
12
13
  - verify and parse incoming WazzAPI webhooks
@@ -94,6 +95,19 @@ with WazzapiClient(api_key="your-api-key") as client:
94
95
 
95
96
  for contact in response.contacts:
96
97
  print(contact.model_dump())
98
+
99
+ ### List devices
100
+
101
+ ```python
102
+ from wazzapi import WazzapiClient
103
+
104
+ with WazzapiClient(api_key="your-api-key") as client:
105
+ response = client.devices.list(limit=20, status="connected")
106
+
107
+ for device in response.devices:
108
+ print(device.name, device.session_name, device.status)
109
+ ```
110
+
97
111
  ```
98
112
 
99
113
  ### Create a template
@@ -158,6 +172,7 @@ Supported webhook event families:
158
172
 
159
173
  Ready-to-run examples live in `examples/`:
160
174
 
175
+ - `examples/list_devices.py`
161
176
  - `examples/list_contacts.py`
162
177
  - `examples/send_message.py`
163
178
  - `examples/create_template.py`
@@ -168,6 +183,7 @@ Run them with:
168
183
 
169
184
  ```bash
170
185
  uv run python examples/list_contacts.py
186
+ uv run python examples/list_devices.py
171
187
  uv run python examples/send_message.py
172
188
  uv run python examples/create_template.py
173
189
  uv run python examples/preview_template.py
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from wazzapi import WazzapiClient
6
+
7
+ api_key = os.environ["WAZZAPI_API_KEY"]
8
+
9
+ with WazzapiClient(api_key=api_key) as client:
10
+ response = client.devices.list(limit=20, status="connected")
11
+
12
+ for device in response.devices:
13
+ print(device.model_dump())
14
+
15
+ print({"total": response.total, "limit": response.limit, "offset": response.offset})
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "wazzapi"
3
- version = "0.1.2"
3
+ version = "0.3.0"
4
4
  description = "Typed Python SDK for WazzAPI"
5
5
  readme = { file = "README.md", content-type = "text/markdown" }
6
6
  requires-python = ">=3.10"
@@ -25,8 +25,8 @@ dependencies = [
25
25
  ]
26
26
 
27
27
  [project.urls]
28
- Homepage = "https://github.com/wazzapihq/wazzapi-py"
29
28
  API = "https://api.wazzapi.com"
29
+ Homepage = "https://github.com/wazzapihq/wazzapi-py"
30
30
 
31
31
  [dependency-groups]
32
32
  dev = [
@@ -10,7 +10,7 @@ from wazzapi import (
10
10
 
11
11
 
12
12
  def main() -> None:
13
- assert __version__ == "0.1.2"
13
+ assert __version__ == "0.3.0"
14
14
 
15
15
  client = WazzapiClient(api_key="smoke-test-token")
16
16
  try:
@@ -15,6 +15,7 @@ def test_client_formats_bearer_auth_header() -> None:
15
15
  assert client.http.headers["Authorization"] == "Bearer plain-token"
16
16
  assert client.http.headers["Accept"] == "application/json"
17
17
  assert client.http.headers["Content-Type"] == "application/json"
18
+ assert client.devices is not None
18
19
  finally:
19
20
  client.close()
20
21
 
@@ -0,0 +1,285 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import Callable
5
+
6
+ import httpx
7
+ import pytest
8
+
9
+ from wazzapi import WazzapiClient, models
10
+
11
+
12
+ @pytest.mark.parametrize(
13
+ ("call_factory", "expected_method", "expected_path", "expected_response_type", "response_json"),
14
+ [
15
+ (
16
+ lambda client: client.groups.list(session_name="main", limit=25),
17
+ "GET",
18
+ "/api/v1/groups",
19
+ models.GroupListResponse,
20
+ {"groups": [], "total": 0},
21
+ ),
22
+ (
23
+ lambda client: client.groups.get("123@g.us", session_name="main"),
24
+ "GET",
25
+ "/api/v1/groups/123@g.us",
26
+ models.GroupDetailResponse,
27
+ {"id": "123@g.us", "name": "Test Group"},
28
+ ),
29
+ (
30
+ lambda client: client.groups.get_participants("123@g.us", session_name="main"),
31
+ "GET",
32
+ "/api/v1/groups/123@g.us/participants",
33
+ models.GroupParticipantsResponse,
34
+ {"participants": [{"id": "6281234567890@s.whatsapp.net", "is_admin": True}]},
35
+ ),
36
+ (
37
+ lambda client: client.groups.create({"session_name": "main", "name": "New Group", "participants": ["6281234567890"]}),
38
+ "POST",
39
+ "/api/v1/groups/create",
40
+ models.CreateGroupResponseModel,
41
+ {"success": True, "jid": "456@g.us", "name": "New Group"},
42
+ ),
43
+ (
44
+ lambda client: client.groups.send_text({"session_name": "main", "group_jid": "123@g.us", "text": "hello"}),
45
+ "POST",
46
+ "/api/v1/groups/send",
47
+ models.SendGroupResponse,
48
+ {"success": True, "message_id": "msg_1", "status": "queued"},
49
+ ),
50
+ (
51
+ lambda client: client.groups.send_media({
52
+ "session_name": "main",
53
+ "group_jid": "123@g.us",
54
+ "media_url": "https://example.com/img.jpg",
55
+ "media_type": "image",
56
+ }),
57
+ "POST",
58
+ "/api/v1/groups/send/media",
59
+ models.SendGroupResponse,
60
+ {"success": True, "message_id": "msg_2", "status": "queued"},
61
+ ),
62
+ (
63
+ lambda client: client.groups.update_participants({
64
+ "session_name": "main",
65
+ "group_jid": "123@g.us",
66
+ "action": "promote",
67
+ "participants": ["6281234567890@s.whatsapp.net"],
68
+ }),
69
+ "POST",
70
+ "/api/v1/groups/participants",
71
+ models.GroupOperationResponseModel,
72
+ {"success": True, "details": "done"},
73
+ ),
74
+ (
75
+ lambda client: client.groups.add_participant("123@g.us", session_name="main", participant_jid="6281234567890@s.whatsapp.net"),
76
+ "POST",
77
+ "/api/v1/groups/123@g.us/participants/add",
78
+ models.GroupOperationResponseModel,
79
+ {"success": True, "details": "added"},
80
+ ),
81
+ (
82
+ lambda client: client.groups.remove_participant("123@g.us", session_name="main", participant_jid="6281234567890@s.whatsapp.net"),
83
+ "POST",
84
+ "/api/v1/groups/123@g.us/participants/remove",
85
+ models.GroupOperationResponseModel,
86
+ {"success": True, "details": "removed"},
87
+ ),
88
+ (
89
+ lambda client: client.groups.get_invite_link("123@g.us", session_name="main"),
90
+ "POST",
91
+ "/api/v1/groups/123@g.us/invite-link",
92
+ models.GroupInviteLinkResponseModel,
93
+ {"success": True, "invite_link": "https://chat.whatsapp.com/abc"},
94
+ ),
95
+ (
96
+ lambda client: client.groups.get_invite_info({"session_name": "main", "invite_link": "https://chat.whatsapp.com/abc"}),
97
+ "POST",
98
+ "/api/v1/groups/invite-info",
99
+ models.GroupInviteInfoResponseModel,
100
+ {"success": True, "jid": "789@g.us", "name": "Invited Group"},
101
+ ),
102
+ (
103
+ lambda client: client.groups.join({"session_name": "main", "invite_link": "https://chat.whatsapp.com/abc"}),
104
+ "POST",
105
+ "/api/v1/groups/join",
106
+ models.GroupOperationResponseModel,
107
+ {"success": True, "details": "joined"},
108
+ ),
109
+ (
110
+ lambda client: client.groups.leave("123@g.us", session_name="main"),
111
+ "POST",
112
+ "/api/v1/groups/123@g.us/leave",
113
+ models.GroupOperationResponseModel,
114
+ {"success": True, "details": "left"},
115
+ ),
116
+ (
117
+ lambda client: client.groups.set_name("123@g.us", {"session_name": "main", "name": "Renamed"}),
118
+ "POST",
119
+ "/api/v1/groups/123@g.us/name",
120
+ models.GroupOperationResponseModel,
121
+ {"success": True, "details": "name updated"},
122
+ ),
123
+ (
124
+ lambda client: client.groups.set_topic("123@g.us", {"session_name": "main", "topic": "new topic"}),
125
+ "POST",
126
+ "/api/v1/groups/123@g.us/topic",
127
+ models.GroupOperationResponseModel,
128
+ {"success": True, "details": "topic updated"},
129
+ ),
130
+ (
131
+ lambda client: client.groups.set_photo("123@g.us", {"session_name": "main", "image_data_uri": "data:image/png;base64,abc"}),
132
+ "POST",
133
+ "/api/v1/groups/123@g.us/photo",
134
+ models.GroupOperationResponseModel,
135
+ {"success": True, "details": "photo updated"},
136
+ ),
137
+ (
138
+ lambda client: client.groups.set_announce("123@g.us", {"session_name": "main", "announce": True}),
139
+ "POST",
140
+ "/api/v1/groups/123@g.us/announce",
141
+ models.GroupOperationResponseModel,
142
+ {"success": True, "details": "announce set"},
143
+ ),
144
+ (
145
+ lambda client: client.groups.set_locked("123@g.us", {"session_name": "main", "locked": True}),
146
+ "POST",
147
+ "/api/v1/groups/123@g.us/locked",
148
+ models.GroupOperationResponseModel,
149
+ {"success": True, "details": "locked set"},
150
+ ),
151
+ (
152
+ lambda client: client.groups.set_ephemeral("123@g.us", {"session_name": "main", "duration": "24h"}),
153
+ "POST",
154
+ "/api/v1/groups/123@g.us/ephemeral",
155
+ models.GroupOperationResponseModel,
156
+ {"success": True, "details": "ephemeral set"},
157
+ ),
158
+ ],
159
+ )
160
+ def test_groups_methods_hit_expected_routes(
161
+ call_factory: Callable[[WazzapiClient], object],
162
+ expected_method: str,
163
+ expected_path: str,
164
+ expected_response_type: type[object],
165
+ response_json: dict[str, object],
166
+ ) -> None:
167
+ seen: dict[str, object] = {}
168
+
169
+ def handler(request: httpx.Request) -> httpx.Response:
170
+ seen["method"] = request.method
171
+ seen["path"] = request.url.path
172
+ seen["query"] = dict(request.url.params)
173
+ seen["body"] = json.loads(request.content.decode("utf-8")) if request.content else None
174
+ status_code = 201 if request.method == "POST" else 200
175
+ return httpx.Response(status_code, json=response_json)
176
+
177
+ transport = httpx.MockTransport(handler)
178
+ with WazzapiClient(base_url="https://api.example.com", transport=transport) as client:
179
+ result = call_factory(client)
180
+
181
+ assert seen["method"] == expected_method
182
+ assert seen["path"] == expected_path
183
+ assert isinstance(result, expected_response_type)
184
+
185
+
186
+ def test_groups_remove_photo_returns_operation_response() -> None:
187
+ seen: dict[str, object] = {}
188
+
189
+ def handler(request: httpx.Request) -> httpx.Response:
190
+ seen["method"] = request.method
191
+ seen["path"] = request.url.path
192
+ seen["query"] = dict(request.url.params)
193
+ return httpx.Response(
194
+ 200,
195
+ json={"success": True, "details": "photo removed"},
196
+ )
197
+
198
+ transport = httpx.MockTransport(handler)
199
+ with WazzapiClient(base_url="https://api.example.com", transport=transport) as client:
200
+ result = client.groups.remove_photo("123@g.us", session_name="main")
201
+
202
+ assert seen["method"] == "DELETE"
203
+ assert seen["path"] == "/api/v1/groups/123@g.us/photo"
204
+ assert seen["query"] == {"session_name": "main"}
205
+ assert isinstance(result, models.GroupOperationResponseModel)
206
+ assert result.success is True
207
+
208
+
209
+ def test_groups_list_sends_query_params() -> None:
210
+ seen: dict[str, object] = {}
211
+
212
+ def handler(request: httpx.Request) -> httpx.Response:
213
+ seen["query"] = dict(request.url.params)
214
+ return httpx.Response(200, json={"groups": [], "total": 0})
215
+
216
+ transport = httpx.MockTransport(handler)
217
+ with WazzapiClient(base_url="https://api.example.com", transport=transport) as client:
218
+ client.groups.list(session_name="main", limit=10, offset=5)
219
+
220
+ assert seen["query"] == {"session_name": "main", "limit": "10", "offset": "5"}
221
+
222
+
223
+ def test_groups_get_sends_session_name_query() -> None:
224
+ seen: dict[str, object] = {}
225
+
226
+ def handler(request: httpx.Request) -> httpx.Response:
227
+ seen["query"] = dict(request.url.params)
228
+ return httpx.Response(200, json={"id": "123@g.us", "name": "Test"})
229
+
230
+ transport = httpx.MockTransport(handler)
231
+ with WazzapiClient(base_url="https://api.example.com", transport=transport) as client:
232
+ client.groups.get("123@g.us", session_name="main")
233
+
234
+ assert seen["query"] == {"session_name": "main"}
235
+
236
+
237
+ def test_groups_add_participant_sends_query_params() -> None:
238
+ seen: dict[str, object] = {}
239
+
240
+ def handler(request: httpx.Request) -> httpx.Response:
241
+ seen["query"] = dict(request.url.params)
242
+ return httpx.Response(200, json={"success": True, "details": "added"})
243
+
244
+ transport = httpx.MockTransport(handler)
245
+ with WazzapiClient(base_url="https://api.example.com", transport=transport) as client:
246
+ client.groups.add_participant(
247
+ "123@g.us",
248
+ session_name="main",
249
+ participant_jid="6281234567890@s.whatsapp.net",
250
+ )
251
+
252
+ assert seen["query"] == {
253
+ "session_name": "main",
254
+ "participant_jid": "6281234567890@s.whatsapp.net",
255
+ }
256
+
257
+
258
+ def test_groups_models_are_importable() -> None:
259
+ from wazzapi.models import (
260
+ CreateGroupRequest,
261
+ CreateGroupResponseModel,
262
+ GroupDetailResponse,
263
+ GroupInviteInfoResponseModel,
264
+ GroupInviteLinkResponseModel,
265
+ GroupListItem,
266
+ GroupListResponse,
267
+ GroupOperationResponseModel,
268
+ GroupParticipantItem,
269
+ GroupParticipantsResponse,
270
+ InviteInfoRequest,
271
+ JoinGroupRequest,
272
+ SendGroupMediaRequest,
273
+ SendGroupResponse,
274
+ SendGroupTextRequest,
275
+ SetGroupAnnounceRequest,
276
+ SetGroupEphemeralRequest,
277
+ SetGroupLockedRequest,
278
+ SetGroupNameRequest,
279
+ SetGroupPhotoRequest,
280
+ SetGroupTopicRequest,
281
+ UpdateParticipantsRequest,
282
+ )
283
+
284
+ assert GroupListItem is not None
285
+ assert CreateGroupResponseModel is not None
@@ -12,6 +12,18 @@ from wazzapi import WazzapiClient, models
12
12
  @pytest.mark.parametrize(
13
13
  ("call_factory", "expected_method", "expected_path", "expected_response_type", "response_json"),
14
14
  [
15
+ (
16
+ lambda client: client.devices.list(limit=25, status="connected", search="main"),
17
+ "GET",
18
+ "/api/v1/devices",
19
+ models.DeviceListResponse,
20
+ {
21
+ "devices": [],
22
+ "total": 0,
23
+ "limit": 25,
24
+ "offset": 0,
25
+ },
26
+ ),
15
27
  (
16
28
  lambda client: client.contacts.list(limit=25, search="alice"),
17
29
  "GET",
@@ -134,6 +146,50 @@ def test_delete_endpoints_return_none() -> None:
134
146
  assert seen_paths == ["/api/v1/contacts/contact_1", "/api/v1/templates/template_1"]
135
147
 
136
148
 
149
+ def test_get_device_returns_typed_response() -> None:
150
+ seen: dict[str, object] = {}
151
+
152
+ def handler(request: httpx.Request) -> httpx.Response:
153
+ seen["method"] = request.method
154
+ seen["path"] = request.url.path
155
+ return httpx.Response(
156
+ 200,
157
+ json={
158
+ "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
159
+ "name": "Primary Device",
160
+ "session_name": "main",
161
+ "status": "connected",
162
+ "is_primary": True,
163
+ "auto_warmer_enabled": True,
164
+ "total_messages_sent": 123,
165
+ "total_messages_received": 45,
166
+ "contacts_synced_count": 900,
167
+ "created_at": "2026-05-01T00:00:00Z",
168
+ "updated_at": "2026-05-02T00:00:00Z",
169
+ "timezone": "Asia/Jakarta",
170
+ "connection_attempts": 2,
171
+ "daily_message_limit": 1000,
172
+ "message_delay_ms": 500,
173
+ "phone_number": "6281234567890",
174
+ "last_connected_at": "2026-05-02T00:00:00Z",
175
+ "last_health_check_at": "2026-05-02T01:00:00Z",
176
+ "last_contact_sync_at": "2026-05-02T02:00:00Z",
177
+ "last_contact_sync_status": "completed",
178
+ "backend_id": "7a5d6343-1dd1-429a-8d83-25c9bf06c878",
179
+ },
180
+ )
181
+
182
+ transport = httpx.MockTransport(handler)
183
+ with WazzapiClient(base_url="https://api.example.com", transport=transport) as client:
184
+ result = client.devices.get("3c90c3cc-0d44-4b50-8888-8dd25736052a")
185
+
186
+ assert seen["method"] == "GET"
187
+ assert seen["path"] == "/api/v1/devices/3c90c3cc-0d44-4b50-8888-8dd25736052a"
188
+ assert isinstance(result, models.DeviceResponse)
189
+ assert result.session_name == "main"
190
+ assert result.timezone == "Asia/Jakarta"
191
+
192
+
137
193
  def test_sync_status_returns_typed_list() -> None:
138
194
  def handler(_: httpx.Request) -> httpx.Response:
139
195
  return httpx.Response(
@@ -16,7 +16,7 @@ from .webhooks import (
16
16
  verify_webhook_signature,
17
17
  )
18
18
 
19
- __version__ = "0.1.2"
19
+ __version__ = "0.3.0"
20
20
 
21
21
  __all__ = [
22
22
  "EVENT_HEADER",
@@ -7,7 +7,7 @@ import httpx
7
7
  from pydantic import BaseModel, TypeAdapter
8
8
 
9
9
  from .errors import WazzapiAPIError
10
- from .resources import ContactsResource, MessagesResource, TemplatesResource
10
+ from .resources import ContactsResource, DevicesResource, GroupsResource, MessagesResource, TemplatesResource
11
11
 
12
12
  DEFAULT_BASE_URL = "https://api.wazzapi.com"
13
13
 
@@ -39,6 +39,8 @@ class WazzapiClient:
39
39
  )
40
40
 
41
41
  self.contacts = ContactsResource(self)
42
+ self.devices = DevicesResource(self)
43
+ self.groups = GroupsResource(self)
42
44
  self.messages = MessagesResource(self)
43
45
  self.templates = TemplatesResource(self)
44
46
 
@@ -1,6 +1,10 @@
1
1
  from .base import WazzapiModel
2
2
  from .contacts import *
3
3
  from .contacts import __all__ as contacts_all
4
+ from .devices import *
5
+ from .devices import __all__ as devices_all
6
+ from .groups import *
7
+ from .groups import __all__ as groups_all
4
8
  from .media import *
5
9
  from .media import __all__ as media_all
6
10
  from .messages import *
@@ -10,4 +14,13 @@ from .templates import __all__ as templates_all
10
14
  from .webhooks import *
11
15
  from .webhooks import __all__ as webhooks_all
12
16
 
13
- __all__ = ["WazzapiModel", *contacts_all, *media_all, *messages_all, *templates_all, *webhooks_all]
17
+ __all__ = [
18
+ "WazzapiModel",
19
+ *contacts_all,
20
+ *devices_all,
21
+ *groups_all,
22
+ *media_all,
23
+ *messages_all,
24
+ *templates_all,
25
+ *webhooks_all,
26
+ ]
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+
5
+ from .base import WazzapiModel
6
+
7
+
8
+ class DeviceItem(WazzapiModel):
9
+ id: str
10
+ name: str
11
+ session_name: str
12
+ status: str
13
+ is_primary: bool
14
+ auto_warmer_enabled: bool
15
+ total_messages_sent: int
16
+ total_messages_received: int
17
+ contacts_synced_count: int
18
+ created_at: datetime
19
+ updated_at: datetime
20
+ phone_number: str | None = None
21
+ last_connected_at: datetime | None = None
22
+ last_health_check_at: datetime | None = None
23
+ last_contact_sync_at: datetime | None = None
24
+ last_contact_sync_status: str | None = None
25
+
26
+
27
+ class DeviceListResponse(WazzapiModel):
28
+ devices: list[DeviceItem]
29
+ total: int
30
+ limit: int
31
+ offset: int
32
+
33
+
34
+ class DeviceResponse(DeviceItem):
35
+ timezone: str
36
+ connection_attempts: int
37
+ daily_message_limit: int
38
+ message_delay_ms: int
39
+ backend_id: str | None = None
40
+
41
+
42
+ __all__ = ["DeviceItem", "DeviceListResponse", "DeviceResponse"]
@@ -0,0 +1,165 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Any
5
+
6
+ from .base import WazzapiModel
7
+
8
+
9
+ class GroupListItem(WazzapiModel):
10
+ id: str
11
+ name: str
12
+ description: str | None = None
13
+ owner: str | None = None
14
+ participants_count: int = 0
15
+
16
+
17
+ class GroupListResponse(WazzapiModel):
18
+ groups: list[GroupListItem]
19
+ total: int
20
+
21
+
22
+ class GroupDetailResponse(WazzapiModel):
23
+ id: str
24
+ name: str
25
+ description: str | None = None
26
+ owner: str | None = None
27
+ participants_count: int = 0
28
+
29
+
30
+ class GroupParticipantItem(WazzapiModel):
31
+ id: str
32
+ is_admin: bool = False
33
+ is_super_admin: bool = False
34
+
35
+
36
+ class GroupParticipantsResponse(WazzapiModel):
37
+ participants: list[GroupParticipantItem]
38
+
39
+
40
+ class GroupOperationResponseModel(WazzapiModel):
41
+ success: bool
42
+ details: str
43
+
44
+
45
+ class CreateGroupRequest(WazzapiModel):
46
+ session_name: str
47
+ name: str
48
+ participants: list[str]
49
+
50
+
51
+ class CreateGroupResponseModel(WazzapiModel):
52
+ success: bool
53
+ jid: str
54
+ name: str
55
+ owner_jid: str | None = None
56
+ group_created: str | None = None
57
+
58
+
59
+ class SendGroupTextRequest(WazzapiModel):
60
+ session_name: str
61
+ group_jid: str
62
+ text: str
63
+ message_id: str | None = None
64
+
65
+
66
+ class SendGroupMediaRequest(WazzapiModel):
67
+ session_name: str
68
+ group_jid: str
69
+ media_url: str
70
+ media_type: str
71
+ caption: str | None = None
72
+ filename: str | None = None
73
+
74
+
75
+ class SendGroupResponse(WazzapiModel):
76
+ success: bool
77
+ message_id: str
78
+ status: str
79
+ run_id: str | None = None
80
+
81
+
82
+ class UpdateParticipantsRequest(WazzapiModel):
83
+ session_name: str
84
+ group_jid: str
85
+ action: str
86
+ participants: list[str]
87
+
88
+
89
+ class GroupInviteLinkResponseModel(WazzapiModel):
90
+ success: bool
91
+ invite_link: str
92
+
93
+
94
+ class GroupInviteInfoResponseModel(WazzapiModel):
95
+ success: bool
96
+ jid: str
97
+ name: str
98
+ participants: int = 0
99
+ owner_jid: str | None = None
100
+
101
+
102
+ class InviteInfoRequest(WazzapiModel):
103
+ session_name: str
104
+ invite_link: str
105
+
106
+
107
+ class JoinGroupRequest(WazzapiModel):
108
+ session_name: str
109
+ invite_link: str
110
+
111
+
112
+ class SetGroupNameRequest(WazzapiModel):
113
+ session_name: str
114
+ name: str
115
+
116
+
117
+ class SetGroupTopicRequest(WazzapiModel):
118
+ session_name: str
119
+ topic: str
120
+
121
+
122
+ class SetGroupPhotoRequest(WazzapiModel):
123
+ session_name: str
124
+ image_data_uri: str
125
+
126
+
127
+ class SetGroupAnnounceRequest(WazzapiModel):
128
+ session_name: str
129
+ announce: bool
130
+
131
+
132
+ class SetGroupLockedRequest(WazzapiModel):
133
+ session_name: str
134
+ locked: bool
135
+
136
+
137
+ class SetGroupEphemeralRequest(WazzapiModel):
138
+ session_name: str
139
+ duration: str
140
+
141
+
142
+ __all__ = [
143
+ "CreateGroupRequest",
144
+ "CreateGroupResponseModel",
145
+ "GroupDetailResponse",
146
+ "GroupInviteInfoResponseModel",
147
+ "GroupInviteLinkResponseModel",
148
+ "GroupListItem",
149
+ "GroupListResponse",
150
+ "GroupOperationResponseModel",
151
+ "GroupParticipantItem",
152
+ "GroupParticipantsResponse",
153
+ "InviteInfoRequest",
154
+ "JoinGroupRequest",
155
+ "SendGroupMediaRequest",
156
+ "SendGroupResponse",
157
+ "SendGroupTextRequest",
158
+ "SetGroupAnnounceRequest",
159
+ "SetGroupEphemeralRequest",
160
+ "SetGroupLockedRequest",
161
+ "SetGroupNameRequest",
162
+ "SetGroupPhotoRequest",
163
+ "SetGroupTopicRequest",
164
+ "UpdateParticipantsRequest",
165
+ ]
@@ -0,0 +1,13 @@
1
+ from .contacts import ContactsResource
2
+ from .devices import DevicesResource
3
+ from .groups import GroupsResource
4
+ from .messages import MessagesResource
5
+ from .templates import TemplatesResource
6
+
7
+ __all__ = [
8
+ "ContactsResource",
9
+ "DevicesResource",
10
+ "GroupsResource",
11
+ "MessagesResource",
12
+ "TemplatesResource",
13
+ ]
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from .. import models
4
+ from .base import BaseResource
5
+
6
+
7
+ class DevicesResource(BaseResource):
8
+ def list(
9
+ self,
10
+ *,
11
+ limit: int | None = None,
12
+ offset: int | None = None,
13
+ status: str | None = None,
14
+ search: str | None = None,
15
+ sort_by: str | None = None,
16
+ sort_order: str | None = None,
17
+ ) -> models.DeviceListResponse:
18
+ return self._client._request(
19
+ "GET",
20
+ "/api/v1/devices",
21
+ params={
22
+ "limit": limit,
23
+ "offset": offset,
24
+ "status": status,
25
+ "search": search,
26
+ "sort_by": sort_by,
27
+ "sort_order": sort_order,
28
+ },
29
+ response_model=models.DeviceListResponse,
30
+ )
31
+
32
+ def get(self, device_id: str) -> models.DeviceResponse:
33
+ return self._client._request(
34
+ "GET",
35
+ f"/api/v1/devices/{device_id}",
36
+ response_model=models.DeviceResponse,
37
+ )
38
+
39
+
40
+ __all__ = ["DevicesResource"]
@@ -0,0 +1,266 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .. import models
6
+ from .base import BaseResource
7
+
8
+
9
+ class GroupsResource(BaseResource):
10
+ def list(
11
+ self,
12
+ *,
13
+ session_name: str,
14
+ limit: int | None = None,
15
+ offset: int | None = None,
16
+ ) -> models.GroupListResponse:
17
+ return self._client._request(
18
+ "GET",
19
+ "/api/v1/groups",
20
+ params={
21
+ "session_name": session_name,
22
+ "limit": limit,
23
+ "offset": offset,
24
+ },
25
+ response_model=models.GroupListResponse,
26
+ )
27
+
28
+ def get(
29
+ self,
30
+ group_jid: str,
31
+ *,
32
+ session_name: str,
33
+ ) -> models.GroupDetailResponse:
34
+ return self._client._request(
35
+ "GET",
36
+ f"/api/v1/groups/{group_jid}",
37
+ params={"session_name": session_name},
38
+ response_model=models.GroupDetailResponse,
39
+ )
40
+
41
+ def get_participants(
42
+ self,
43
+ group_jid: str,
44
+ *,
45
+ session_name: str,
46
+ ) -> models.GroupParticipantsResponse:
47
+ return self._client._request(
48
+ "GET",
49
+ f"/api/v1/groups/{group_jid}/participants",
50
+ params={"session_name": session_name},
51
+ response_model=models.GroupParticipantsResponse,
52
+ )
53
+
54
+ def create(
55
+ self,
56
+ payload: models.CreateGroupRequest | dict[str, Any],
57
+ ) -> models.CreateGroupResponseModel:
58
+ return self._client._request(
59
+ "POST",
60
+ "/api/v1/groups/create",
61
+ json_body=payload,
62
+ response_model=models.CreateGroupResponseModel,
63
+ )
64
+
65
+ def send_text(
66
+ self,
67
+ payload: models.SendGroupTextRequest | dict[str, Any],
68
+ ) -> models.SendGroupResponse:
69
+ return self._client._request(
70
+ "POST",
71
+ "/api/v1/groups/send",
72
+ json_body=payload,
73
+ response_model=models.SendGroupResponse,
74
+ )
75
+
76
+ def send_media(
77
+ self,
78
+ payload: models.SendGroupMediaRequest | dict[str, Any],
79
+ ) -> models.SendGroupResponse:
80
+ return self._client._request(
81
+ "POST",
82
+ "/api/v1/groups/send/media",
83
+ json_body=payload,
84
+ response_model=models.SendGroupResponse,
85
+ )
86
+
87
+ def update_participants(
88
+ self,
89
+ payload: models.UpdateParticipantsRequest | dict[str, Any],
90
+ ) -> models.GroupOperationResponseModel:
91
+ return self._client._request(
92
+ "POST",
93
+ "/api/v1/groups/participants",
94
+ json_body=payload,
95
+ response_model=models.GroupOperationResponseModel,
96
+ )
97
+
98
+ def add_participant(
99
+ self,
100
+ group_jid: str,
101
+ *,
102
+ session_name: str,
103
+ participant_jid: str,
104
+ ) -> models.GroupOperationResponseModel:
105
+ return self._client._request(
106
+ "POST",
107
+ f"/api/v1/groups/{group_jid}/participants/add",
108
+ params={
109
+ "session_name": session_name,
110
+ "participant_jid": participant_jid,
111
+ },
112
+ response_model=models.GroupOperationResponseModel,
113
+ )
114
+
115
+ def remove_participant(
116
+ self,
117
+ group_jid: str,
118
+ *,
119
+ session_name: str,
120
+ participant_jid: str,
121
+ ) -> models.GroupOperationResponseModel:
122
+ return self._client._request(
123
+ "POST",
124
+ f"/api/v1/groups/{group_jid}/participants/remove",
125
+ params={
126
+ "session_name": session_name,
127
+ "participant_jid": participant_jid,
128
+ },
129
+ response_model=models.GroupOperationResponseModel,
130
+ )
131
+
132
+ def get_invite_link(
133
+ self,
134
+ group_jid: str,
135
+ *,
136
+ session_name: str,
137
+ ) -> models.GroupInviteLinkResponseModel:
138
+ return self._client._request(
139
+ "POST",
140
+ f"/api/v1/groups/{group_jid}/invite-link",
141
+ params={"session_name": session_name},
142
+ response_model=models.GroupInviteLinkResponseModel,
143
+ )
144
+
145
+ def get_invite_info(
146
+ self,
147
+ payload: models.InviteInfoRequest | dict[str, Any],
148
+ ) -> models.GroupInviteInfoResponseModel:
149
+ return self._client._request(
150
+ "POST",
151
+ "/api/v1/groups/invite-info",
152
+ json_body=payload,
153
+ response_model=models.GroupInviteInfoResponseModel,
154
+ )
155
+
156
+ def join(
157
+ self,
158
+ payload: models.JoinGroupRequest | dict[str, Any],
159
+ ) -> models.GroupOperationResponseModel:
160
+ return self._client._request(
161
+ "POST",
162
+ "/api/v1/groups/join",
163
+ json_body=payload,
164
+ response_model=models.GroupOperationResponseModel,
165
+ )
166
+
167
+ def leave(
168
+ self,
169
+ group_jid: str,
170
+ *,
171
+ session_name: str,
172
+ ) -> models.GroupOperationResponseModel:
173
+ return self._client._request(
174
+ "POST",
175
+ f"/api/v1/groups/{group_jid}/leave",
176
+ params={"session_name": session_name},
177
+ response_model=models.GroupOperationResponseModel,
178
+ )
179
+
180
+ def set_name(
181
+ self,
182
+ group_jid: str,
183
+ payload: models.SetGroupNameRequest | dict[str, Any],
184
+ ) -> models.GroupOperationResponseModel:
185
+ return self._client._request(
186
+ "POST",
187
+ f"/api/v1/groups/{group_jid}/name",
188
+ json_body=payload,
189
+ response_model=models.GroupOperationResponseModel,
190
+ )
191
+
192
+ def set_topic(
193
+ self,
194
+ group_jid: str,
195
+ payload: models.SetGroupTopicRequest | dict[str, Any],
196
+ ) -> models.GroupOperationResponseModel:
197
+ return self._client._request(
198
+ "POST",
199
+ f"/api/v1/groups/{group_jid}/topic",
200
+ json_body=payload,
201
+ response_model=models.GroupOperationResponseModel,
202
+ )
203
+
204
+ def set_photo(
205
+ self,
206
+ group_jid: str,
207
+ payload: models.SetGroupPhotoRequest | dict[str, Any],
208
+ ) -> models.GroupOperationResponseModel:
209
+ return self._client._request(
210
+ "POST",
211
+ f"/api/v1/groups/{group_jid}/photo",
212
+ json_body=payload,
213
+ response_model=models.GroupOperationResponseModel,
214
+ )
215
+
216
+ def remove_photo(
217
+ self,
218
+ group_jid: str,
219
+ *,
220
+ session_name: str,
221
+ ) -> models.GroupOperationResponseModel:
222
+ return self._client._request(
223
+ "DELETE",
224
+ f"/api/v1/groups/{group_jid}/photo",
225
+ params={"session_name": session_name},
226
+ response_model=models.GroupOperationResponseModel,
227
+ )
228
+
229
+ def set_announce(
230
+ self,
231
+ group_jid: str,
232
+ payload: models.SetGroupAnnounceRequest | dict[str, Any],
233
+ ) -> models.GroupOperationResponseModel:
234
+ return self._client._request(
235
+ "POST",
236
+ f"/api/v1/groups/{group_jid}/announce",
237
+ json_body=payload,
238
+ response_model=models.GroupOperationResponseModel,
239
+ )
240
+
241
+ def set_locked(
242
+ self,
243
+ group_jid: str,
244
+ payload: models.SetGroupLockedRequest | dict[str, Any],
245
+ ) -> models.GroupOperationResponseModel:
246
+ return self._client._request(
247
+ "POST",
248
+ f"/api/v1/groups/{group_jid}/locked",
249
+ json_body=payload,
250
+ response_model=models.GroupOperationResponseModel,
251
+ )
252
+
253
+ def set_ephemeral(
254
+ self,
255
+ group_jid: str,
256
+ payload: models.SetGroupEphemeralRequest | dict[str, Any],
257
+ ) -> models.GroupOperationResponseModel:
258
+ return self._client._request(
259
+ "POST",
260
+ f"/api/v1/groups/{group_jid}/ephemeral",
261
+ json_body=payload,
262
+ response_model=models.GroupOperationResponseModel,
263
+ )
264
+
265
+
266
+ __all__ = ["GroupsResource"]
@@ -1,5 +0,0 @@
1
- from .contacts import ContactsResource
2
- from .messages import MessagesResource
3
- from .templates import TemplatesResource
4
-
5
- __all__ = ["ContactsResource", "MessagesResource", "TemplatesResource"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes