multiwa 1.0.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.
- multiwa/__init__.py +31 -0
- multiwa/broadcasts.py +147 -0
- multiwa/client.py +173 -0
- multiwa/contacts.py +172 -0
- multiwa/messages.py +322 -0
- multiwa/profiles.py +161 -0
- multiwa/py.typed +0 -0
- multiwa/types.py +159 -0
- multiwa/webhooks.py +162 -0
- multiwa-1.0.0.dist-info/METADATA +177 -0
- multiwa-1.0.0.dist-info/RECORD +13 -0
- multiwa-1.0.0.dist-info/WHEEL +4 -0
- multiwa-1.0.0.dist-info/licenses/LICENSE +21 -0
multiwa/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MultiWA Python SDK
|
|
3
|
+
|
|
4
|
+
Official Python SDK for MultiWA - WhatsApp Business API Gateway.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .client import MultiWA, AsyncMultiWA
|
|
8
|
+
from .webhooks import verify_webhook_signature
|
|
9
|
+
from .types import (
|
|
10
|
+
Message,
|
|
11
|
+
MessageResult,
|
|
12
|
+
Contact,
|
|
13
|
+
Broadcast,
|
|
14
|
+
Template,
|
|
15
|
+
Profile,
|
|
16
|
+
Webhook,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__version__ = "1.0.0"
|
|
20
|
+
__all__ = [
|
|
21
|
+
"MultiWA",
|
|
22
|
+
"AsyncMultiWA",
|
|
23
|
+
"Message",
|
|
24
|
+
"MessageResult",
|
|
25
|
+
"Contact",
|
|
26
|
+
"Broadcast",
|
|
27
|
+
"Template",
|
|
28
|
+
"Profile",
|
|
29
|
+
"Webhook",
|
|
30
|
+
"verify_webhook_signature",
|
|
31
|
+
]
|
multiwa/broadcasts.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MultiWA Python SDK - Broadcasts Client
|
|
3
|
+
|
|
4
|
+
Client for managing broadcast campaigns.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, List, Dict, Any
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .types import Broadcast
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BroadcastsClient:
|
|
14
|
+
"""Synchronous Broadcasts API client"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, http: httpx.Client):
|
|
17
|
+
self._http = http
|
|
18
|
+
|
|
19
|
+
def create(
|
|
20
|
+
self,
|
|
21
|
+
profile_id: str,
|
|
22
|
+
name: str,
|
|
23
|
+
recipients: List[str],
|
|
24
|
+
template_id: Optional[str] = None,
|
|
25
|
+
message: Optional[str] = None,
|
|
26
|
+
schedule_at: Optional[str] = None,
|
|
27
|
+
) -> Broadcast:
|
|
28
|
+
"""Create a new broadcast campaign"""
|
|
29
|
+
response = self._http.post("/broadcasts", json={
|
|
30
|
+
"profileId": profile_id,
|
|
31
|
+
"name": name,
|
|
32
|
+
"recipients": recipients,
|
|
33
|
+
"templateId": template_id,
|
|
34
|
+
"message": message,
|
|
35
|
+
"scheduleAt": schedule_at,
|
|
36
|
+
})
|
|
37
|
+
response.raise_for_status()
|
|
38
|
+
return Broadcast(**response.json())
|
|
39
|
+
|
|
40
|
+
def get(self, broadcast_id: str) -> Broadcast:
|
|
41
|
+
"""Get a broadcast by ID"""
|
|
42
|
+
response = self._http.get(f"/broadcasts/{broadcast_id}")
|
|
43
|
+
response.raise_for_status()
|
|
44
|
+
return Broadcast(**response.json())
|
|
45
|
+
|
|
46
|
+
def list(
|
|
47
|
+
self,
|
|
48
|
+
profile_id: str,
|
|
49
|
+
limit: int = 50,
|
|
50
|
+
offset: int = 0,
|
|
51
|
+
status: Optional[str] = None,
|
|
52
|
+
) -> List[Broadcast]:
|
|
53
|
+
"""List broadcasts for a profile"""
|
|
54
|
+
params: Dict[str, Any] = {
|
|
55
|
+
"limit": limit,
|
|
56
|
+
"offset": offset,
|
|
57
|
+
}
|
|
58
|
+
if status:
|
|
59
|
+
params["status"] = status
|
|
60
|
+
|
|
61
|
+
response = self._http.get(f"/broadcasts/profile/{profile_id}", params=params)
|
|
62
|
+
response.raise_for_status()
|
|
63
|
+
return [Broadcast(**b) for b in response.json()]
|
|
64
|
+
|
|
65
|
+
def start(self, broadcast_id: str) -> Broadcast:
|
|
66
|
+
"""Start a broadcast"""
|
|
67
|
+
response = self._http.post(f"/broadcasts/{broadcast_id}/start")
|
|
68
|
+
response.raise_for_status()
|
|
69
|
+
return Broadcast(**response.json())
|
|
70
|
+
|
|
71
|
+
def pause(self, broadcast_id: str) -> Broadcast:
|
|
72
|
+
"""Pause a broadcast"""
|
|
73
|
+
response = self._http.post(f"/broadcasts/{broadcast_id}/pause")
|
|
74
|
+
response.raise_for_status()
|
|
75
|
+
return Broadcast(**response.json())
|
|
76
|
+
|
|
77
|
+
def cancel(self, broadcast_id: str) -> Broadcast:
|
|
78
|
+
"""Cancel a broadcast"""
|
|
79
|
+
response = self._http.post(f"/broadcasts/{broadcast_id}/cancel")
|
|
80
|
+
response.raise_for_status()
|
|
81
|
+
return Broadcast(**response.json())
|
|
82
|
+
|
|
83
|
+
def delete(self, broadcast_id: str) -> Dict[str, bool]:
|
|
84
|
+
"""Delete a broadcast"""
|
|
85
|
+
response = self._http.delete(f"/broadcasts/{broadcast_id}")
|
|
86
|
+
response.raise_for_status()
|
|
87
|
+
return response.json()
|
|
88
|
+
|
|
89
|
+
def stats(self, broadcast_id: str) -> Dict[str, Any]:
|
|
90
|
+
"""Get broadcast statistics"""
|
|
91
|
+
response = self._http.get(f"/broadcasts/{broadcast_id}/stats")
|
|
92
|
+
response.raise_for_status()
|
|
93
|
+
return response.json()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class AsyncBroadcastsClient:
|
|
97
|
+
"""Asynchronous Broadcasts API client"""
|
|
98
|
+
|
|
99
|
+
def __init__(self, http: httpx.AsyncClient):
|
|
100
|
+
self._http = http
|
|
101
|
+
|
|
102
|
+
async def create(
|
|
103
|
+
self,
|
|
104
|
+
profile_id: str,
|
|
105
|
+
name: str,
|
|
106
|
+
recipients: List[str],
|
|
107
|
+
template_id: Optional[str] = None,
|
|
108
|
+
message: Optional[str] = None,
|
|
109
|
+
schedule_at: Optional[str] = None,
|
|
110
|
+
) -> Broadcast:
|
|
111
|
+
"""Create a new broadcast campaign"""
|
|
112
|
+
response = await self._http.post("/broadcasts", json={
|
|
113
|
+
"profileId": profile_id,
|
|
114
|
+
"name": name,
|
|
115
|
+
"recipients": recipients,
|
|
116
|
+
"templateId": template_id,
|
|
117
|
+
"message": message,
|
|
118
|
+
"scheduleAt": schedule_at,
|
|
119
|
+
})
|
|
120
|
+
response.raise_for_status()
|
|
121
|
+
return Broadcast(**response.json())
|
|
122
|
+
|
|
123
|
+
async def get(self, broadcast_id: str) -> Broadcast:
|
|
124
|
+
"""Get a broadcast by ID"""
|
|
125
|
+
response = await self._http.get(f"/broadcasts/{broadcast_id}")
|
|
126
|
+
response.raise_for_status()
|
|
127
|
+
return Broadcast(**response.json())
|
|
128
|
+
|
|
129
|
+
async def list(
|
|
130
|
+
self,
|
|
131
|
+
profile_id: str,
|
|
132
|
+
limit: int = 50,
|
|
133
|
+
offset: int = 0,
|
|
134
|
+
) -> List[Broadcast]:
|
|
135
|
+
"""List broadcasts for a profile"""
|
|
136
|
+
response = await self._http.get(
|
|
137
|
+
f"/broadcasts/profile/{profile_id}",
|
|
138
|
+
params={"limit": limit, "offset": offset}
|
|
139
|
+
)
|
|
140
|
+
response.raise_for_status()
|
|
141
|
+
return [Broadcast(**b) for b in response.json()]
|
|
142
|
+
|
|
143
|
+
async def start(self, broadcast_id: str) -> Broadcast:
|
|
144
|
+
"""Start a broadcast"""
|
|
145
|
+
response = await self._http.post(f"/broadcasts/{broadcast_id}/start")
|
|
146
|
+
response.raise_for_status()
|
|
147
|
+
return Broadcast(**response.json())
|
multiwa/client.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MultiWA Python SDK - Core Client
|
|
3
|
+
|
|
4
|
+
Main client class for synchronous and asynchronous API access.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Dict, Any
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .messages import MessagesClient, AsyncMessagesClient
|
|
11
|
+
from .contacts import ContactsClient, AsyncContactsClient
|
|
12
|
+
from .broadcasts import BroadcastsClient, AsyncBroadcastsClient
|
|
13
|
+
from .profiles import ProfilesClient, AsyncProfilesClient
|
|
14
|
+
from .webhooks import WebhooksClient, AsyncWebhooksClient
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _normalize_base_url(base_url: str) -> str:
|
|
18
|
+
"""Return ``base_url`` with exactly one trailing slash.
|
|
19
|
+
|
|
20
|
+
httpx joins ``base_url`` with the request path using URL semantics, so the
|
|
21
|
+
configured base must end with a slash. Request paths must be relative
|
|
22
|
+
(no leading slash) for the base path such as ``/api/v1`` to survive.
|
|
23
|
+
"""
|
|
24
|
+
return base_url.rstrip("/") + "/"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _MultiWAHttpClient(httpx.Client):
|
|
28
|
+
"""httpx.Client variant that preserves the API base path.
|
|
29
|
+
|
|
30
|
+
A request path that begins with ``/`` is treated by URL semantics as
|
|
31
|
+
absolute and would drop the ``/api/v1`` prefix configured on the base
|
|
32
|
+
URL. Existing endpoint methods in this SDK call the HTTP client with
|
|
33
|
+
leading-slash paths (for example ``/messages/text``). To keep them
|
|
34
|
+
working while still preserving the configured base path, we trim the
|
|
35
|
+
leading slash before httpx builds the request.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def build_request(self, method, url, **kwargs): # type: ignore[override]
|
|
39
|
+
if isinstance(url, str) and url.startswith("/"):
|
|
40
|
+
url = url.lstrip("/")
|
|
41
|
+
return super().build_request(method, url, **kwargs)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _MultiWAAsyncHttpClient(httpx.AsyncClient):
|
|
45
|
+
"""Async variant of ``_MultiWAHttpClient`` with the same behavior."""
|
|
46
|
+
|
|
47
|
+
def build_request(self, method, url, **kwargs): # type: ignore[override]
|
|
48
|
+
if isinstance(url, str) and url.startswith("/"):
|
|
49
|
+
url = url.lstrip("/")
|
|
50
|
+
return super().build_request(method, url, **kwargs)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MultiWA:
|
|
54
|
+
"""
|
|
55
|
+
Synchronous MultiWA API Client
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
client = MultiWA(
|
|
59
|
+
base_url="https://your-instance.com/api/v1",
|
|
60
|
+
api_key="your-api-key",
|
|
61
|
+
)
|
|
62
|
+
result = client.messages.send_text(profile_id, to, "Hello!")
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
base_url: str,
|
|
68
|
+
api_key: str,
|
|
69
|
+
timeout: float = 30.0,
|
|
70
|
+
):
|
|
71
|
+
# Keep the externally-visible base_url unchanged (no trailing slash),
|
|
72
|
+
# but pass a trailing-slash version to httpx so the API prefix is
|
|
73
|
+
# preserved when endpoints use either absolute or relative paths.
|
|
74
|
+
self.base_url = base_url.rstrip("/")
|
|
75
|
+
self.api_key = api_key
|
|
76
|
+
self.timeout = timeout
|
|
77
|
+
|
|
78
|
+
self._http = _MultiWAHttpClient(
|
|
79
|
+
base_url=_normalize_base_url(base_url),
|
|
80
|
+
headers={
|
|
81
|
+
"X-API-Key": api_key,
|
|
82
|
+
"Content-Type": "application/json",
|
|
83
|
+
},
|
|
84
|
+
timeout=timeout,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Initialize API clients
|
|
88
|
+
self.messages = MessagesClient(self._http)
|
|
89
|
+
self.contacts = ContactsClient(self._http)
|
|
90
|
+
self.broadcasts = BroadcastsClient(self._http)
|
|
91
|
+
self.profiles = ProfilesClient(self._http)
|
|
92
|
+
self.webhooks = WebhooksClient(self._http)
|
|
93
|
+
|
|
94
|
+
def close(self):
|
|
95
|
+
"""Close the HTTP client"""
|
|
96
|
+
self._http.close()
|
|
97
|
+
|
|
98
|
+
def __enter__(self):
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
def __exit__(self, *args):
|
|
102
|
+
self.close()
|
|
103
|
+
|
|
104
|
+
def request(
|
|
105
|
+
self,
|
|
106
|
+
method: str,
|
|
107
|
+
path: str,
|
|
108
|
+
**kwargs
|
|
109
|
+
) -> Dict[str, Any]:
|
|
110
|
+
"""Make a raw API request"""
|
|
111
|
+
response = self._http.request(method, path, **kwargs)
|
|
112
|
+
response.raise_for_status()
|
|
113
|
+
return response.json()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class AsyncMultiWA:
|
|
117
|
+
"""
|
|
118
|
+
Asynchronous MultiWA API Client
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
async with AsyncMultiWA(
|
|
122
|
+
base_url="https://your-instance.com/api/v1",
|
|
123
|
+
api_key="your-api-key",
|
|
124
|
+
) as client:
|
|
125
|
+
result = await client.messages.send_text(profile_id, to, "Hello!")
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
base_url: str,
|
|
131
|
+
api_key: str,
|
|
132
|
+
timeout: float = 30.0,
|
|
133
|
+
):
|
|
134
|
+
self.base_url = base_url.rstrip("/")
|
|
135
|
+
self.api_key = api_key
|
|
136
|
+
self.timeout = timeout
|
|
137
|
+
|
|
138
|
+
self._http = _MultiWAAsyncHttpClient(
|
|
139
|
+
base_url=_normalize_base_url(base_url),
|
|
140
|
+
headers={
|
|
141
|
+
"X-API-Key": api_key,
|
|
142
|
+
"Content-Type": "application/json",
|
|
143
|
+
},
|
|
144
|
+
timeout=timeout,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Initialize API clients
|
|
148
|
+
self.messages = AsyncMessagesClient(self._http)
|
|
149
|
+
self.contacts = AsyncContactsClient(self._http)
|
|
150
|
+
self.broadcasts = AsyncBroadcastsClient(self._http)
|
|
151
|
+
self.profiles = AsyncProfilesClient(self._http)
|
|
152
|
+
self.webhooks = AsyncWebhooksClient(self._http)
|
|
153
|
+
|
|
154
|
+
async def close(self):
|
|
155
|
+
"""Close the HTTP client"""
|
|
156
|
+
await self._http.aclose()
|
|
157
|
+
|
|
158
|
+
async def __aenter__(self):
|
|
159
|
+
return self
|
|
160
|
+
|
|
161
|
+
async def __aexit__(self, *args):
|
|
162
|
+
await self.close()
|
|
163
|
+
|
|
164
|
+
async def request(
|
|
165
|
+
self,
|
|
166
|
+
method: str,
|
|
167
|
+
path: str,
|
|
168
|
+
**kwargs
|
|
169
|
+
) -> Dict[str, Any]:
|
|
170
|
+
"""Make a raw API request"""
|
|
171
|
+
response = await self._http.request(method, path, **kwargs)
|
|
172
|
+
response.raise_for_status()
|
|
173
|
+
return response.json()
|
multiwa/contacts.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MultiWA Python SDK - Contacts Client
|
|
3
|
+
|
|
4
|
+
Client for managing WhatsApp contacts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, List, Dict, Any
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .types import Contact
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ContactsClient:
|
|
14
|
+
"""Synchronous Contacts API client"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, http: httpx.Client):
|
|
17
|
+
self._http = http
|
|
18
|
+
|
|
19
|
+
def create(
|
|
20
|
+
self,
|
|
21
|
+
profile_id: str,
|
|
22
|
+
phone: str,
|
|
23
|
+
name: Optional[str] = None,
|
|
24
|
+
email: Optional[str] = None,
|
|
25
|
+
tags: Optional[List[str]] = None,
|
|
26
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
27
|
+
) -> Contact:
|
|
28
|
+
"""Create a new contact"""
|
|
29
|
+
response = self._http.post("/contacts", json={
|
|
30
|
+
"profileId": profile_id,
|
|
31
|
+
"phone": phone,
|
|
32
|
+
"name": name,
|
|
33
|
+
"email": email,
|
|
34
|
+
"tags": tags or [],
|
|
35
|
+
"metadata": metadata or {},
|
|
36
|
+
})
|
|
37
|
+
response.raise_for_status()
|
|
38
|
+
return Contact(**response.json())
|
|
39
|
+
|
|
40
|
+
def get(self, contact_id: str) -> Contact:
|
|
41
|
+
"""Get a contact by ID"""
|
|
42
|
+
response = self._http.get(f"/contacts/{contact_id}")
|
|
43
|
+
response.raise_for_status()
|
|
44
|
+
return Contact(**response.json())
|
|
45
|
+
|
|
46
|
+
def update(
|
|
47
|
+
self,
|
|
48
|
+
contact_id: str,
|
|
49
|
+
name: Optional[str] = None,
|
|
50
|
+
email: Optional[str] = None,
|
|
51
|
+
tags: Optional[List[str]] = None,
|
|
52
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
53
|
+
) -> Contact:
|
|
54
|
+
"""Update a contact"""
|
|
55
|
+
data = {}
|
|
56
|
+
if name is not None:
|
|
57
|
+
data["name"] = name
|
|
58
|
+
if email is not None:
|
|
59
|
+
data["email"] = email
|
|
60
|
+
if tags is not None:
|
|
61
|
+
data["tags"] = tags
|
|
62
|
+
if metadata is not None:
|
|
63
|
+
data["metadata"] = metadata
|
|
64
|
+
|
|
65
|
+
response = self._http.patch(f"/contacts/{contact_id}", json=data)
|
|
66
|
+
response.raise_for_status()
|
|
67
|
+
return Contact(**response.json())
|
|
68
|
+
|
|
69
|
+
def delete(self, contact_id: str) -> Dict[str, bool]:
|
|
70
|
+
"""Delete a contact"""
|
|
71
|
+
response = self._http.delete(f"/contacts/{contact_id}")
|
|
72
|
+
response.raise_for_status()
|
|
73
|
+
return response.json()
|
|
74
|
+
|
|
75
|
+
def list(
|
|
76
|
+
self,
|
|
77
|
+
profile_id: str,
|
|
78
|
+
limit: int = 50,
|
|
79
|
+
offset: int = 0,
|
|
80
|
+
search: Optional[str] = None,
|
|
81
|
+
tags: Optional[List[str]] = None,
|
|
82
|
+
) -> List[Contact]:
|
|
83
|
+
"""List contacts for a profile"""
|
|
84
|
+
params: Dict[str, Any] = {
|
|
85
|
+
"limit": limit,
|
|
86
|
+
"offset": offset,
|
|
87
|
+
}
|
|
88
|
+
if search:
|
|
89
|
+
params["search"] = search
|
|
90
|
+
if tags:
|
|
91
|
+
params["tags"] = ",".join(tags)
|
|
92
|
+
|
|
93
|
+
response = self._http.get(f"/contacts/profile/{profile_id}", params=params)
|
|
94
|
+
response.raise_for_status()
|
|
95
|
+
return [Contact(**c) for c in response.json()]
|
|
96
|
+
|
|
97
|
+
def add_tags(
|
|
98
|
+
self,
|
|
99
|
+
contact_id: str,
|
|
100
|
+
tags: List[str],
|
|
101
|
+
) -> Contact:
|
|
102
|
+
"""Add tags to a contact"""
|
|
103
|
+
response = self._http.post(f"/contacts/{contact_id}/tags", json={
|
|
104
|
+
"tags": tags,
|
|
105
|
+
})
|
|
106
|
+
response.raise_for_status()
|
|
107
|
+
return Contact(**response.json())
|
|
108
|
+
|
|
109
|
+
def remove_tags(
|
|
110
|
+
self,
|
|
111
|
+
contact_id: str,
|
|
112
|
+
tags: List[str],
|
|
113
|
+
) -> Contact:
|
|
114
|
+
"""Remove tags from a contact"""
|
|
115
|
+
response = self._http.delete(f"/contacts/{contact_id}/tags", json={
|
|
116
|
+
"tags": tags,
|
|
117
|
+
})
|
|
118
|
+
response.raise_for_status()
|
|
119
|
+
return Contact(**response.json())
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class AsyncContactsClient:
|
|
123
|
+
"""Asynchronous Contacts API client"""
|
|
124
|
+
|
|
125
|
+
def __init__(self, http: httpx.AsyncClient):
|
|
126
|
+
self._http = http
|
|
127
|
+
|
|
128
|
+
async def create(
|
|
129
|
+
self,
|
|
130
|
+
profile_id: str,
|
|
131
|
+
phone: str,
|
|
132
|
+
name: Optional[str] = None,
|
|
133
|
+
email: Optional[str] = None,
|
|
134
|
+
tags: Optional[List[str]] = None,
|
|
135
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
136
|
+
) -> Contact:
|
|
137
|
+
"""Create a new contact"""
|
|
138
|
+
response = await self._http.post("/contacts", json={
|
|
139
|
+
"profileId": profile_id,
|
|
140
|
+
"phone": phone,
|
|
141
|
+
"name": name,
|
|
142
|
+
"email": email,
|
|
143
|
+
"tags": tags or [],
|
|
144
|
+
"metadata": metadata or {},
|
|
145
|
+
})
|
|
146
|
+
response.raise_for_status()
|
|
147
|
+
return Contact(**response.json())
|
|
148
|
+
|
|
149
|
+
async def get(self, contact_id: str) -> Contact:
|
|
150
|
+
"""Get a contact by ID"""
|
|
151
|
+
response = await self._http.get(f"/contacts/{contact_id}")
|
|
152
|
+
response.raise_for_status()
|
|
153
|
+
return Contact(**response.json())
|
|
154
|
+
|
|
155
|
+
async def list(
|
|
156
|
+
self,
|
|
157
|
+
profile_id: str,
|
|
158
|
+
limit: int = 50,
|
|
159
|
+
offset: int = 0,
|
|
160
|
+
search: Optional[str] = None,
|
|
161
|
+
) -> List[Contact]:
|
|
162
|
+
"""List contacts for a profile"""
|
|
163
|
+
params: Dict[str, Any] = {
|
|
164
|
+
"limit": limit,
|
|
165
|
+
"offset": offset,
|
|
166
|
+
}
|
|
167
|
+
if search:
|
|
168
|
+
params["search"] = search
|
|
169
|
+
|
|
170
|
+
response = await self._http.get(f"/contacts/profile/{profile_id}", params=params)
|
|
171
|
+
response.raise_for_status()
|
|
172
|
+
return [Contact(**c) for c in response.json()]
|