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.
Files changed (38) hide show
  1. {wazzapi-0.3.0 → wazzapi-0.4.0}/PKG-INFO +2 -2
  2. {wazzapi-0.3.0 → wazzapi-0.4.0}/pyproject.toml +4 -4
  3. {wazzapi-0.3.0 → wazzapi-0.4.0}/tests/smoke_test.py +1 -1
  4. {wazzapi-0.3.0 → wazzapi-0.4.0}/tests/test_resources.py +67 -0
  5. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/__init__.py +1 -1
  6. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/client.py +31 -0
  7. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/models/contacts.py +23 -0
  8. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/models/messages.py +3 -0
  9. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/resources/contacts.py +36 -0
  10. {wazzapi-0.3.0 → wazzapi-0.4.0}/.gitignore +0 -0
  11. {wazzapi-0.3.0 → wazzapi-0.4.0}/README.md +0 -0
  12. {wazzapi-0.3.0 → wazzapi-0.4.0}/examples/create_template.py +0 -0
  13. {wazzapi-0.3.0 → wazzapi-0.4.0}/examples/list_contacts.py +0 -0
  14. {wazzapi-0.3.0 → wazzapi-0.4.0}/examples/list_devices.py +0 -0
  15. {wazzapi-0.3.0 → wazzapi-0.4.0}/examples/preview_template.py +0 -0
  16. {wazzapi-0.3.0 → wazzapi-0.4.0}/examples/send_message.py +0 -0
  17. {wazzapi-0.3.0 → wazzapi-0.4.0}/examples/verify_webhook.py +0 -0
  18. {wazzapi-0.3.0 → wazzapi-0.4.0}/tests/test_client.py +0 -0
  19. {wazzapi-0.3.0 → wazzapi-0.4.0}/tests/test_groups.py +0 -0
  20. {wazzapi-0.3.0 → wazzapi-0.4.0}/tests/test_media.py +0 -0
  21. {wazzapi-0.3.0 → wazzapi-0.4.0}/tests/test_webhooks.py +0 -0
  22. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/errors.py +0 -0
  23. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/media.py +0 -0
  24. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/models/__init__.py +0 -0
  25. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/models/base.py +0 -0
  26. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/models/devices.py +0 -0
  27. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/models/groups.py +0 -0
  28. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/models/media.py +0 -0
  29. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/models/templates.py +0 -0
  30. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/models/webhooks.py +0 -0
  31. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/py.typed +0 -0
  32. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/resources/__init__.py +0 -0
  33. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/resources/base.py +0 -0
  34. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/resources/devices.py +0 -0
  35. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/resources/groups.py +0 -0
  36. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/resources/messages.py +0 -0
  37. {wazzapi-0.3.0 → wazzapi-0.4.0}/wazzapi/resources/templates.py +0 -0
  38. {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.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<45.0,>=44.0
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.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>=44.0,<45.0",
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>=8.3,<9.0",
34
- "pytest-asyncio>=0.25,<1.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
 
@@ -10,7 +10,7 @@ from wazzapi import (
10
10
 
11
11
 
12
12
  def main() -> None:
13
- assert __version__ == "0.3.0"
13
+ assert __version__ == "0.4.0"
14
14
 
15
15
  client = WazzapiClient(api_key="smoke-test-token")
16
16
  try:
@@ -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"
@@ -16,7 +16,7 @@ from .webhooks import (
16
16
  verify_webhook_signature,
17
17
  )
18
18
 
19
- __version__ = "0.3.0"
19
+ __version__ = "0.4.0"
20
20
 
21
21
  __all__ = [
22
22
  "EVENT_HEADER",
@@ -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