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/__init__.py ADDED
@@ -0,0 +1,36 @@
1
+ from . import models
2
+ from .client import WazzapiClient
3
+ from .errors import WazzapiAPIError, WazzapiError
4
+ from .models import *
5
+ from .webhooks import (
6
+ EVENT_HEADER,
7
+ EVENT_ID_HEADER,
8
+ SIGNATURE_HEADER,
9
+ WebhookHandler,
10
+ WazzapiWebhookError,
11
+ WazzapiWebhookParseError,
12
+ WazzapiWebhookVerificationError,
13
+ generate_webhook_signature,
14
+ parse_webhook,
15
+ verify_webhook_signature,
16
+ )
17
+
18
+ __version__ = "0.1.0"
19
+
20
+ __all__ = [
21
+ "EVENT_HEADER",
22
+ "EVENT_ID_HEADER",
23
+ "SIGNATURE_HEADER",
24
+ "__version__",
25
+ "generate_webhook_signature",
26
+ "parse_webhook",
27
+ "WazzapiAPIError",
28
+ "WazzapiClient",
29
+ "WazzapiError",
30
+ "WazzapiWebhookError",
31
+ "WazzapiWebhookParseError",
32
+ "WazzapiWebhookVerificationError",
33
+ "verify_webhook_signature",
34
+ "WebhookHandler",
35
+ "models",
36
+ ] + models.__all__
wazzapi/client.py ADDED
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+ import httpx
7
+ from pydantic import BaseModel, TypeAdapter
8
+
9
+ from .errors import WazzapiAPIError
10
+ from .resources import ContactsResource, MessagesResource, TemplatesResource
11
+
12
+ DEFAULT_BASE_URL = "https://api.wazzapi.com"
13
+
14
+
15
+ class WazzapiClient:
16
+ """Synchronous client for the WazzAPI API."""
17
+
18
+ def __init__(
19
+ self,
20
+ *,
21
+ base_url: str = DEFAULT_BASE_URL,
22
+ api_key: str | None = None,
23
+ timeout: float = 30.0,
24
+ headers: Mapping[str, str] | None = None,
25
+ client: httpx.Client | None = None,
26
+ transport: httpx.BaseTransport | None = None,
27
+ ) -> None:
28
+ base_headers = self._build_headers(
29
+ api_key=api_key,
30
+ headers=headers,
31
+ )
32
+
33
+ self._owns_client = client is None
34
+ self._client = client or httpx.Client(
35
+ base_url=base_url.rstrip("/"),
36
+ timeout=timeout,
37
+ headers=base_headers,
38
+ transport=transport,
39
+ )
40
+
41
+ self.contacts = ContactsResource(self)
42
+ self.messages = MessagesResource(self)
43
+ self.templates = TemplatesResource(self)
44
+
45
+ @staticmethod
46
+ def _build_headers(
47
+ *,
48
+ api_key: str | None,
49
+ headers: Mapping[str, str] | None,
50
+ ) -> dict[str, str]:
51
+ merged_headers: dict[str, str] = {
52
+ "Accept": "application/json",
53
+ "Content-Type": "application/json",
54
+ }
55
+
56
+ if headers:
57
+ merged_headers.update(dict(headers))
58
+
59
+ if api_key:
60
+ merged_headers["Authorization"] = (
61
+ api_key if api_key.lower().startswith("bearer ") else f"Bearer {api_key}"
62
+ )
63
+
64
+ return merged_headers
65
+
66
+ @property
67
+ def http(self) -> httpx.Client:
68
+ return self._client
69
+
70
+ def close(self) -> None:
71
+ if self._owns_client:
72
+ self._client.close()
73
+
74
+ def __enter__(self) -> "WazzapiClient":
75
+ return self
76
+
77
+ def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
78
+ self.close()
79
+
80
+ def _request(
81
+ self,
82
+ method: str,
83
+ path: str,
84
+ *,
85
+ params: Mapping[str, Any] | None = None,
86
+ json_body: BaseModel | Mapping[str, Any] | None = None,
87
+ response_model: Any | None = None,
88
+ ) -> Any:
89
+ response = self._client.request(
90
+ method,
91
+ path,
92
+ params=self._filter_none(params),
93
+ json=self._normalize_body(json_body),
94
+ )
95
+
96
+ if response.status_code >= 400:
97
+ raise WazzapiAPIError.from_response(response)
98
+
99
+ if response.status_code == 204 or not response.content:
100
+ return None
101
+
102
+ if response_model is None:
103
+ return response.json()
104
+
105
+ payload = response.json()
106
+ return TypeAdapter(response_model).validate_python(payload)
107
+
108
+ @staticmethod
109
+ def _filter_none(data: Mapping[str, Any] | None) -> dict[str, Any] | None:
110
+ if data is None:
111
+ return None
112
+ return {key: value for key, value in data.items() if value is not None}
113
+
114
+ @staticmethod
115
+ def _normalize_body(data: BaseModel | Mapping[str, Any] | None) -> dict[str, Any] | None:
116
+ if data is None:
117
+ return None
118
+ if isinstance(data, BaseModel):
119
+ return data.model_dump(mode="json", exclude_none=True)
120
+ return {key: value for key, value in data.items() if value is not None}
wazzapi/errors.py ADDED
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+
8
+ class WazzapiError(Exception):
9
+ """Base exception for the WazzAPI SDK."""
10
+
11
+
12
+ class WazzapiAPIError(WazzapiError):
13
+ """Raised when the WazzAPI API returns a non-success response."""
14
+
15
+ def __init__(
16
+ self,
17
+ status_code: int,
18
+ message: str,
19
+ *,
20
+ details: Any | None = None,
21
+ response_text: str | None = None,
22
+ ) -> None:
23
+ super().__init__(f"WazzAPI API error {status_code}: {message}")
24
+ self.status_code = status_code
25
+ self.message = message
26
+ self.details = details
27
+ self.response_text = response_text
28
+
29
+ @classmethod
30
+ def from_response(cls, response: httpx.Response) -> "WazzapiAPIError":
31
+ details: Any | None = None
32
+ message = response.reason_phrase or "Request failed"
33
+
34
+ try:
35
+ payload = response.json()
36
+ except ValueError:
37
+ payload = None
38
+
39
+ if isinstance(payload, dict):
40
+ details = payload.get("detail", payload)
41
+ if isinstance(details, str):
42
+ message = details
43
+ elif isinstance(details, list) and details:
44
+ first_item = details[0]
45
+ if isinstance(first_item, dict):
46
+ message = first_item.get("msg") or message
47
+ else:
48
+ message = str(first_item)
49
+ elif payload.get("message"):
50
+ message = str(payload["message"])
51
+ elif payload is not None:
52
+ details = payload
53
+ message = str(payload)
54
+
55
+ return cls(
56
+ response.status_code,
57
+ message,
58
+ details=details,
59
+ response_text=response.text,
60
+ )
@@ -0,0 +1,11 @@
1
+ from .base import WazzapiModel
2
+ from .contacts import *
3
+ from .contacts import __all__ as contacts_all
4
+ from .messages import *
5
+ from .messages import __all__ as messages_all
6
+ from .templates import *
7
+ from .templates import __all__ as templates_all
8
+ from .webhooks import *
9
+ from .webhooks import __all__ as webhooks_all
10
+
11
+ __all__ = ["WazzapiModel", *contacts_all, *messages_all, *templates_all, *webhooks_all]
wazzapi/models/base.py ADDED
@@ -0,0 +1,10 @@
1
+ from pydantic import BaseModel, ConfigDict
2
+
3
+
4
+ class WazzapiModel(BaseModel):
5
+ """Base model for SDK request and response payloads."""
6
+
7
+ model_config = ConfigDict(extra="allow", populate_by_name=True)
8
+
9
+
10
+ __all__ = ["WazzapiModel"]
@@ -0,0 +1,204 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Any
5
+
6
+ from pydantic import Field
7
+
8
+ from .base import WazzapiModel
9
+
10
+
11
+ class AddToGroupRequest(WazzapiModel):
12
+ contact_ids: list[str]
13
+
14
+
15
+ class AddToGroupResponse(WazzapiModel):
16
+ success: bool
17
+ added: int
18
+ member_count: int
19
+
20
+
21
+ class BulkDeleteRequest(WazzapiModel):
22
+ contact_ids: list[str]
23
+
24
+
25
+ class BulkDeleteResponse(WazzapiModel):
26
+ success: bool
27
+ deleted: int
28
+
29
+
30
+ class CSVExportResponse(WazzapiModel):
31
+ csv_data: str
32
+ count: int
33
+ filename: str
34
+
35
+
36
+ class CSVImportRequest(WazzapiModel):
37
+ csv_content: str
38
+ skip_duplicates: bool | None = None
39
+
40
+
41
+ class CSVImportResponse(WazzapiModel):
42
+ success: bool
43
+ imported: int
44
+ updated: int
45
+ errors: list[str] = Field(default_factory=list)
46
+ rows_processed: int
47
+
48
+
49
+ class ContactCreateRequest(WazzapiModel):
50
+ phone_number: str
51
+ name: str | None = None
52
+ email: str | None = None
53
+ tags: list[str] | None = None
54
+ custom_fields: dict[str, Any] | None = None
55
+
56
+
57
+ class ContactGroupCreateRequest(WazzapiModel):
58
+ name: str
59
+ description: str | None = None
60
+
61
+
62
+ class ContactGroupItem(WazzapiModel):
63
+ id: str
64
+ name: str
65
+ description: str | None = None
66
+ member_count: int
67
+ created_at: datetime
68
+
69
+
70
+ class ContactGroupListResponse(WazzapiModel):
71
+ groups: list[ContactGroupItem]
72
+ total: int
73
+ limit: int
74
+ offset: int
75
+
76
+
77
+ class ContactItem(WazzapiModel):
78
+ id: str
79
+ phone_number: str
80
+ name: str | None = None
81
+ email: str | None = None
82
+ whatsapp_name: str | None = None
83
+ profile_picture_url: str | None = None
84
+ source: str
85
+ source_details: dict[str, Any] | None = None
86
+ is_opted_out: bool
87
+ tags: list[str] = Field(default_factory=list)
88
+ last_message_at: datetime | None = None
89
+ created_at: datetime
90
+
91
+
92
+ class ContactGroupMembersResponse(WazzapiModel):
93
+ group: ContactGroupItem
94
+ contacts: list[ContactItem]
95
+ total: int
96
+ limit: int
97
+ offset: int
98
+
99
+
100
+ class ContactGroupUpdateRequest(WazzapiModel):
101
+ name: str | None = None
102
+ description: str | None = None
103
+
104
+
105
+ class ContactListResponse(WazzapiModel):
106
+ contacts: list[ContactItem]
107
+ total: int
108
+ limit: int
109
+ offset: int
110
+
111
+
112
+ class ContactResponse(WazzapiModel):
113
+ id: str
114
+ phone_number: str
115
+ name: str | None = None
116
+ email: str | None = None
117
+ whatsapp_name: str | None = None
118
+ profile_picture_url: str | None = None
119
+ custom_fields: dict[str, Any] | None = None
120
+ source: str
121
+ source_details: dict[str, Any] | None = None
122
+ is_opted_out: bool
123
+ opted_out_at: datetime | None = None
124
+ tags: list[str] = Field(default_factory=list)
125
+ last_message_at: datetime | None = None
126
+ created_at: datetime
127
+ updated_at: datetime
128
+
129
+
130
+ class ContactSyncHistoryItem(WazzapiModel):
131
+ id: str
132
+ account_id: str
133
+ account_name: str
134
+ sync_type: str
135
+ status: str
136
+ contacts_count: int
137
+ started_at: datetime
138
+ completed_at: datetime | None = None
139
+ error_message: str | None = None
140
+
141
+
142
+ class ContactSyncHistoryResponse(WazzapiModel):
143
+ history: list[ContactSyncHistoryItem]
144
+ total: int
145
+
146
+
147
+ class ContactSyncRequest(WazzapiModel):
148
+ whatsapp_account_id: str
149
+ sync_type: str | None = None
150
+
151
+
152
+ class ContactSyncResponse(WazzapiModel):
153
+ success: bool
154
+ job_id: str | None = None
155
+ message: str
156
+ status: str | None = None
157
+
158
+
159
+ class ContactSyncStatusResponse(WazzapiModel):
160
+ account_id: str
161
+ account_name: str
162
+ last_sync_at: datetime | None = None
163
+ last_sync_status: str | None = None
164
+ contacts_synced_count: int
165
+ can_sync: bool
166
+
167
+
168
+ class ContactUpdateRequest(WazzapiModel):
169
+ name: str | None = None
170
+ email: str | None = None
171
+ tags: list[str] | None = None
172
+ custom_fields: dict[str, Any] | None = None
173
+ is_opted_out: bool | None = None
174
+
175
+
176
+ class RemoveFromGroupRequest(WazzapiModel):
177
+ contact_ids: list[str]
178
+
179
+
180
+ __all__ = [
181
+ "AddToGroupRequest",
182
+ "AddToGroupResponse",
183
+ "BulkDeleteRequest",
184
+ "BulkDeleteResponse",
185
+ "CSVExportResponse",
186
+ "CSVImportRequest",
187
+ "CSVImportResponse",
188
+ "ContactCreateRequest",
189
+ "ContactGroupCreateRequest",
190
+ "ContactGroupItem",
191
+ "ContactGroupListResponse",
192
+ "ContactGroupMembersResponse",
193
+ "ContactGroupUpdateRequest",
194
+ "ContactItem",
195
+ "ContactListResponse",
196
+ "ContactResponse",
197
+ "ContactSyncHistoryItem",
198
+ "ContactSyncHistoryResponse",
199
+ "ContactSyncRequest",
200
+ "ContactSyncResponse",
201
+ "ContactSyncStatusResponse",
202
+ "ContactUpdateRequest",
203
+ "RemoveFromGroupRequest",
204
+ ]
@@ -0,0 +1,173 @@
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 InteractiveButton(WazzapiModel):
10
+ id: str
11
+ title: str
12
+
13
+
14
+ class ButtonReplyRequest(WazzapiModel):
15
+ phone_number: str
16
+ body: str
17
+ buttons: list[InteractiveButton]
18
+ whatsapp_account_id: str
19
+ footer: str | None = None
20
+ contact_id: str | None = None
21
+
22
+
23
+ class CancelMessageResponse(WazzapiModel):
24
+ success: bool
25
+ message_id: str
26
+ status: str
27
+
28
+
29
+ class InteractiveMessageResponse(WazzapiModel):
30
+ success: bool
31
+ message_id: str
32
+ status: str
33
+ run_id: str | None = None
34
+
35
+
36
+ class InteractiveRow(WazzapiModel):
37
+ id: str
38
+ title: str
39
+ description: str | None = None
40
+
41
+
42
+ class InteractiveSection(WazzapiModel):
43
+ title: str
44
+ rows: list[InteractiveRow]
45
+
46
+
47
+ class ListReplyRequest(WazzapiModel):
48
+ phone_number: str
49
+ body: str
50
+ button_text: str
51
+ sections: list[InteractiveSection]
52
+ whatsapp_account_id: str
53
+ footer: str | None = None
54
+ contact_id: str | None = None
55
+
56
+
57
+ class MessageItem(WazzapiModel):
58
+ id: str
59
+ phone_number: str
60
+ content: str
61
+ message_type: str = "text"
62
+ media_type: str | None = None
63
+ media_url: str | None = None
64
+ status: str
65
+ direction: str
66
+ retry_count: int
67
+ contact_id: str | None = None
68
+ contact_name: str | None = None
69
+ whatsapp_account_id: str
70
+ campaign_id: str | None = None
71
+ batch_id: str | None = None
72
+ scheduled_for: datetime | None = None
73
+ created_at: datetime
74
+ sent_at: datetime | None = None
75
+ delivered_at: datetime | None = None
76
+ read_at: datetime | None = None
77
+
78
+
79
+ class MessageListResponse(WazzapiModel):
80
+ messages: list[MessageItem]
81
+ total_count: int
82
+ has_more: bool
83
+ current_page: int
84
+ total_pages: int
85
+
86
+
87
+ class MessageResponse(WazzapiModel):
88
+ id: str
89
+ phone_number: str
90
+ content: str
91
+ message_type: str = "text"
92
+ media_type: str | None = None
93
+ media_url: str | None = None
94
+ status: str
95
+ direction: str
96
+ failure_reason: str | None = None
97
+ retry_count: int
98
+ whatsapp_message_id: str | None = None
99
+ variable_values: dict[str, Any] | None = None
100
+ contact_id: str | None = None
101
+ contact_name: str | None = None
102
+ whatsapp_account_id: str
103
+ campaign_id: str | None = None
104
+ batch_id: str | None = None
105
+ scheduled_for: datetime | None = None
106
+ queued_at: datetime | None = None
107
+ sent_at: datetime | None = None
108
+ delivered_at: datetime | None = None
109
+ read_at: datetime | None = None
110
+ failed_at: datetime | None = None
111
+ created_at: datetime
112
+ updated_at: datetime
113
+
114
+
115
+ class MessageStatsResponse(WazzapiModel):
116
+ total: int
117
+ by_status: dict[str, int]
118
+ by_direction: dict[str, int]
119
+ last_7_days: int
120
+ last_30_days: int
121
+
122
+
123
+ class RetryMessageResponse(WazzapiModel):
124
+ success: bool
125
+ message_id: str
126
+ run_id: str | None = None
127
+ status: str
128
+
129
+
130
+ class SendMessageRequest(WazzapiModel):
131
+ phone_number: str
132
+ whatsapp_account_id: str
133
+ content: str | None = None
134
+ template_id: str | None = None
135
+ custom_variables: dict[str, Any] | None = None
136
+ message_type: str = "text"
137
+ media_type: str | None = None
138
+ media_url: str | None = None
139
+ caption: str | None = None
140
+ latitude: float | None = None
141
+ longitude: float | None = None
142
+ location_title: str | None = None
143
+ location_address: str | None = None
144
+ contacts: list[dict[str, Any]] | None = None
145
+ contact_id: str | None = None
146
+ scheduled_for: datetime | None = None
147
+ validate_phone: bool = False
148
+ status_callback_url: str | None = None
149
+
150
+
151
+ class SendMessageResponse(WazzapiModel):
152
+ success: bool
153
+ message_id: str
154
+ status: str
155
+ run_id: str | None = None
156
+
157
+
158
+ __all__ = [
159
+ "ButtonReplyRequest",
160
+ "CancelMessageResponse",
161
+ "InteractiveButton",
162
+ "InteractiveMessageResponse",
163
+ "InteractiveRow",
164
+ "InteractiveSection",
165
+ "ListReplyRequest",
166
+ "MessageItem",
167
+ "MessageListResponse",
168
+ "MessageResponse",
169
+ "MessageStatsResponse",
170
+ "RetryMessageResponse",
171
+ "SendMessageRequest",
172
+ "SendMessageResponse",
173
+ ]
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Any
5
+
6
+ from pydantic import Field
7
+
8
+ from .base import WazzapiModel
9
+
10
+
11
+ class BuiltinVariableInfo(WazzapiModel):
12
+ name: str
13
+ description: str
14
+ example: str
15
+
16
+
17
+ class BuiltinVariablesResponse(WazzapiModel):
18
+ variables: list[BuiltinVariableInfo]
19
+
20
+
21
+ class TemplateCreateRequest(WazzapiModel):
22
+ name: str
23
+ content: str
24
+ category: str = "marketing"
25
+ media_type: str | None = None
26
+ media_url: str | None = None
27
+
28
+
29
+ class TemplateItem(WazzapiModel):
30
+ id: str
31
+ name: str
32
+ category: str
33
+ content: str
34
+ variables: list[str] = Field(default_factory=list)
35
+ builtin_variables: list[str] = Field(default_factory=list)
36
+ custom_variables: list[str] = Field(default_factory=list)
37
+ media_type: str | None = None
38
+ media_url: str | None = None
39
+ times_used: int
40
+ last_used_at: datetime | None = None
41
+ created_at: datetime
42
+
43
+
44
+ class TemplateListResponse(WazzapiModel):
45
+ data: list[TemplateItem]
46
+ pagination: dict[str, Any]
47
+
48
+
49
+ class TemplatePreviewRequest(WazzapiModel):
50
+ content: str | None = None
51
+ template_id: str | None = None
52
+ custom_variables: dict[str, Any] | None = None
53
+ contact_id: str | None = None
54
+
55
+
56
+ class TemplatePreviewResponse(WazzapiModel):
57
+ preview: str
58
+ all_variables: list[str] = Field(default_factory=list)
59
+ builtin_variables: list[str] = Field(default_factory=list)
60
+ custom_variables: list[str] = Field(default_factory=list)
61
+ missing_variables: list[str] = Field(default_factory=list)
62
+
63
+
64
+ class TemplateResponse(WazzapiModel):
65
+ id: str
66
+ name: str
67
+ category: str
68
+ content: str
69
+ variables: list[str] = Field(default_factory=list)
70
+ builtin_variables: list[str] = Field(default_factory=list)
71
+ custom_variables: list[str] = Field(default_factory=list)
72
+ media_type: str | None = None
73
+ media_url: str | None = None
74
+ is_active: bool
75
+ times_used: int
76
+ last_used_at: datetime | None = None
77
+ created_at: datetime
78
+ updated_at: datetime
79
+
80
+
81
+ class TemplateUpdateRequest(WazzapiModel):
82
+ name: str | None = None
83
+ content: str | None = None
84
+ category: str | None = None
85
+ is_active: bool | None = None
86
+ media_type: str | None = None
87
+ media_url: str | None = None
88
+
89
+
90
+ __all__ = [
91
+ "BuiltinVariableInfo",
92
+ "BuiltinVariablesResponse",
93
+ "TemplateCreateRequest",
94
+ "TemplateItem",
95
+ "TemplateListResponse",
96
+ "TemplatePreviewRequest",
97
+ "TemplatePreviewResponse",
98
+ "TemplateResponse",
99
+ "TemplateUpdateRequest",
100
+ ]