wazzapi 0.3.0__tar.gz → 0.4.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.
- {wazzapi-0.3.0 → wazzapi-0.4.0}/PKG-INFO +2 -2
- {wazzapi-0.3.0 → wazzapi-0.4.0}/pyproject.toml +4 -4
- {wazzapi-0.3.0 → wazzapi-0.4.0}/tests/smoke_test.py +1 -1
- {wazzapi-0.3.0 → wazzapi-0.4.0}/tests/test_resources.py +67 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/__init__.py +1 -1
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/client.py +31 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/models/contacts.py +23 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/models/messages.py +3 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/resources/contacts.py +36 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/.gitignore +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/README.md +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/examples/create_template.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/examples/list_contacts.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/examples/list_devices.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/examples/preview_template.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/examples/send_message.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/examples/verify_webhook.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/tests/test_client.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/tests/test_groups.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/tests/test_media.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/tests/test_webhooks.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/errors.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/media.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/models/__init__.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/models/base.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/models/devices.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/models/groups.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/models/media.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/models/templates.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/models/webhooks.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/py.typed +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/resources/__init__.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/resources/base.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/resources/devices.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/resources/groups.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/resources/messages.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/resources/templates.py +0 -0
- {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/webhooks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wazzapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Typed Python SDK for WazzAPI
|
|
5
5
|
Project-URL: API, https://api.wazzapi.com
|
|
6
6
|
Project-URL: Homepage, https://github.com/wazzapihq/wazzapi-py
|
|
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
17
17
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
18
|
Classifier: Typing :: Typed
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
|
-
Requires-Dist: cryptography<
|
|
20
|
+
Requires-Dist: cryptography<50.0,>=46.0
|
|
21
21
|
Requires-Dist: httpx<1.0,>=0.28
|
|
22
22
|
Requires-Dist: pydantic<3.0,>=2.11
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "wazzapi"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.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"
|
|
@@ -19,7 +19,7 @@ classifiers = [
|
|
|
19
19
|
"Typing :: Typed",
|
|
20
20
|
]
|
|
21
21
|
dependencies = [
|
|
22
|
-
"cryptography>=
|
|
22
|
+
"cryptography>=46.0,<50.0",
|
|
23
23
|
"httpx>=0.28,<1.0",
|
|
24
24
|
"pydantic>=2.11,<3.0",
|
|
25
25
|
]
|
|
@@ -30,8 +30,8 @@ Homepage = "https://github.com/wazzapihq/wazzapi-py"
|
|
|
30
30
|
|
|
31
31
|
[dependency-groups]
|
|
32
32
|
dev = [
|
|
33
|
-
"pytest>=
|
|
34
|
-
"pytest-asyncio>=0
|
|
33
|
+
"pytest>=9.0,<10.0",
|
|
34
|
+
"pytest-asyncio>=1.0,<2.0",
|
|
35
35
|
"twine>=6.1,<7.0",
|
|
36
36
|
]
|
|
37
37
|
|
|
@@ -213,3 +213,70 @@ def test_sync_status_returns_typed_list() -> None:
|
|
|
213
213
|
assert len(result) == 1
|
|
214
214
|
assert isinstance(result[0], models.ContactSyncStatusResponse)
|
|
215
215
|
assert result[0].account_id == "wa_123"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_get_profile_picture_returns_metadata() -> None:
|
|
219
|
+
seen: dict[str, object] = {}
|
|
220
|
+
|
|
221
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
222
|
+
seen["method"] = request.method
|
|
223
|
+
seen["path"] = request.url.path
|
|
224
|
+
seen["query"] = dict(request.url.params)
|
|
225
|
+
return httpx.Response(
|
|
226
|
+
200,
|
|
227
|
+
json={
|
|
228
|
+
"contact_id": "contact_1",
|
|
229
|
+
"phone_number": "+6281234567890",
|
|
230
|
+
"url": "https://cdn.example.com/pic.jpg",
|
|
231
|
+
"picture_id": "pic_1",
|
|
232
|
+
"picture_type": "image",
|
|
233
|
+
"preview": True,
|
|
234
|
+
"whatsapp_account_id": "wa_1",
|
|
235
|
+
"cached": False,
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
transport = httpx.MockTransport(handler)
|
|
240
|
+
with WazzapiClient(base_url="https://api.example.com", transport=transport) as client:
|
|
241
|
+
result = client.contacts.get_profile_picture(
|
|
242
|
+
"contact_1",
|
|
243
|
+
whatsapp_account_id="wa_1",
|
|
244
|
+
preview=True,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
assert seen["method"] == "GET"
|
|
248
|
+
assert seen["path"] == "/api/v1/contacts/contact_1/profile-picture"
|
|
249
|
+
assert seen["query"] == {"whatsapp_account_id": "wa_1", "preview": "true"}
|
|
250
|
+
assert isinstance(result, models.ContactProfilePictureResponse)
|
|
251
|
+
assert result.url == "https://cdn.example.com/pic.jpg"
|
|
252
|
+
assert result.preview is True
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_get_profile_picture_image_returns_bytes() -> None:
|
|
256
|
+
seen: dict[str, object] = {}
|
|
257
|
+
image_bytes = b"\xff\xd8\xff\xe0fake-jpeg"
|
|
258
|
+
|
|
259
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
260
|
+
seen["method"] = request.method
|
|
261
|
+
seen["path"] = request.url.path
|
|
262
|
+
seen["query"] = dict(request.url.params)
|
|
263
|
+
return httpx.Response(
|
|
264
|
+
200,
|
|
265
|
+
content=image_bytes,
|
|
266
|
+
headers={"content-type": "image/jpeg"},
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
transport = httpx.MockTransport(handler)
|
|
270
|
+
with WazzapiClient(base_url="https://api.example.com", transport=transport) as client:
|
|
271
|
+
result = client.contacts.get_profile_picture_image(
|
|
272
|
+
"contact_1",
|
|
273
|
+
proxy=True,
|
|
274
|
+
preview=False,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
assert seen["method"] == "GET"
|
|
278
|
+
assert seen["path"] == "/api/v1/contacts/contact_1/profile-picture/image"
|
|
279
|
+
assert seen["query"] == {"proxy": "true", "preview": "false"}
|
|
280
|
+
assert isinstance(result, models.ContactProfilePictureImageResult)
|
|
281
|
+
assert result.content == image_bytes
|
|
282
|
+
assert result.content_type == "image/jpeg"
|
|
@@ -107,6 +107,37 @@ class WazzapiClient:
|
|
|
107
107
|
payload = response.json()
|
|
108
108
|
return TypeAdapter(response_model).validate_python(payload)
|
|
109
109
|
|
|
110
|
+
def _request_bytes(
|
|
111
|
+
self,
|
|
112
|
+
method: str,
|
|
113
|
+
path: str,
|
|
114
|
+
*,
|
|
115
|
+
params: Mapping[str, Any] | None = None,
|
|
116
|
+
response_model: Any | None = None,
|
|
117
|
+
) -> Any:
|
|
118
|
+
"""Issue a request and return binary content (follows redirects)."""
|
|
119
|
+
|
|
120
|
+
response = self._client.request(
|
|
121
|
+
method,
|
|
122
|
+
path,
|
|
123
|
+
params=self._filter_none(params),
|
|
124
|
+
follow_redirects=True,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if response.status_code >= 400:
|
|
128
|
+
raise WazzapiAPIError.from_response(response)
|
|
129
|
+
|
|
130
|
+
payload = {
|
|
131
|
+
"content": response.content,
|
|
132
|
+
"content_type": response.headers.get("content-type"),
|
|
133
|
+
"final_url": str(response.url) if response.url else None,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if response_model is None:
|
|
137
|
+
return payload
|
|
138
|
+
|
|
139
|
+
return TypeAdapter(response_model).validate_python(payload)
|
|
140
|
+
|
|
110
141
|
@staticmethod
|
|
111
142
|
def _filter_none(data: Mapping[str, Any] | None) -> dict[str, Any] | None:
|
|
112
143
|
if data is None:
|
|
@@ -173,6 +173,27 @@ class ContactUpdateRequest(WazzapiModel):
|
|
|
173
173
|
is_opted_out: bool | None = None
|
|
174
174
|
|
|
175
175
|
|
|
176
|
+
class ContactProfilePictureResponse(WazzapiModel):
|
|
177
|
+
"""Ephemeral WhatsApp profile picture metadata for a contact."""
|
|
178
|
+
|
|
179
|
+
contact_id: str
|
|
180
|
+
phone_number: str
|
|
181
|
+
url: str
|
|
182
|
+
picture_id: str | None = None
|
|
183
|
+
picture_type: str | None = None
|
|
184
|
+
preview: bool
|
|
185
|
+
whatsapp_account_id: str
|
|
186
|
+
cached: bool = False
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class ContactProfilePictureImageResult(WazzapiModel):
|
|
190
|
+
"""Raw image bytes returned by the profile-picture image endpoint."""
|
|
191
|
+
|
|
192
|
+
content: bytes
|
|
193
|
+
content_type: str | None = None
|
|
194
|
+
final_url: str | None = None
|
|
195
|
+
|
|
196
|
+
|
|
176
197
|
class RemoveFromGroupRequest(WazzapiModel):
|
|
177
198
|
contact_ids: list[str]
|
|
178
199
|
|
|
@@ -193,6 +214,8 @@ __all__ = [
|
|
|
193
214
|
"ContactGroupUpdateRequest",
|
|
194
215
|
"ContactItem",
|
|
195
216
|
"ContactListResponse",
|
|
217
|
+
"ContactProfilePictureImageResult",
|
|
218
|
+
"ContactProfilePictureResponse",
|
|
196
219
|
"ContactResponse",
|
|
197
220
|
"ContactSyncHistoryItem",
|
|
198
221
|
"ContactSyncHistoryResponse",
|
|
@@ -136,6 +136,9 @@ class SendMessageRequest(WazzapiModel):
|
|
|
136
136
|
message_type: str = "text"
|
|
137
137
|
media_type: str | None = None
|
|
138
138
|
media_url: str | None = None
|
|
139
|
+
media_base64: str | None = None
|
|
140
|
+
filename: str | None = None
|
|
141
|
+
mimetype: str | None = None
|
|
139
142
|
caption: str | None = None
|
|
140
143
|
latitude: float | None = None
|
|
141
144
|
longitude: float | None = None
|
|
@@ -218,5 +218,41 @@ class ContactsResource(BaseResource):
|
|
|
218
218
|
self._client._request("DELETE", f"/api/v1/contacts/{contact_id}")
|
|
219
219
|
return None
|
|
220
220
|
|
|
221
|
+
def get_profile_picture(
|
|
222
|
+
self,
|
|
223
|
+
contact_id: str,
|
|
224
|
+
*,
|
|
225
|
+
whatsapp_account_id: str | None = None,
|
|
226
|
+
preview: bool | None = None,
|
|
227
|
+
) -> models.ContactProfilePictureResponse:
|
|
228
|
+
return self._client._request(
|
|
229
|
+
"GET",
|
|
230
|
+
f"/api/v1/contacts/{contact_id}/profile-picture",
|
|
231
|
+
params={
|
|
232
|
+
"whatsapp_account_id": whatsapp_account_id,
|
|
233
|
+
"preview": preview,
|
|
234
|
+
},
|
|
235
|
+
response_model=models.ContactProfilePictureResponse,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def get_profile_picture_image(
|
|
239
|
+
self,
|
|
240
|
+
contact_id: str,
|
|
241
|
+
*,
|
|
242
|
+
whatsapp_account_id: str | None = None,
|
|
243
|
+
preview: bool | None = None,
|
|
244
|
+
proxy: bool | None = None,
|
|
245
|
+
) -> models.ContactProfilePictureImageResult:
|
|
246
|
+
return self._client._request_bytes(
|
|
247
|
+
"GET",
|
|
248
|
+
f"/api/v1/contacts/{contact_id}/profile-picture/image",
|
|
249
|
+
params={
|
|
250
|
+
"whatsapp_account_id": whatsapp_account_id,
|
|
251
|
+
"preview": preview,
|
|
252
|
+
"proxy": proxy,
|
|
253
|
+
},
|
|
254
|
+
response_model=models.ContactProfilePictureImageResult,
|
|
255
|
+
)
|
|
256
|
+
|
|
221
257
|
|
|
222
258
|
__all__ = ["ContactsResource"]
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|