Skip to content

Commit d90b5ba

Browse files
author
Shanjai
committed
feat: add search, sms, and delivery namespaces to CommuneClient
Adds three new namespaces to both CommuneClient and AsyncCommuneClient that correspond to existing backend API endpoints: - client.search.threads(query, inbox_id, limit) → /v1/search/threads - client.sms.send(to, body) → /v1/sms/send - client.delivery.metrics(inbox_id, period) → /v1/delivery/metrics - client.delivery.suppressions(inbox_id) → /v1/delivery/suppressions - client.delivery.events(inbox_id, event_type) → /v1/delivery/events Also adds five new typed response models: SearchResult, SmsSendResult, DeliveryMetrics, DeliverySuppression, DeliveryEvent. Bumps to 0.3.0.
1 parent 3490a3e commit d90b5ba

File tree

5 files changed

+575
-3
lines changed

5 files changed

+575
-3
lines changed

commune/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
AttachmentUrl,
1919
DomainVerificationResult,
2020
DeleteResult,
21+
SearchResult,
22+
SmsSendResult,
23+
DeliveryMetrics,
24+
DeliverySuppression,
25+
DeliveryEvent,
2126
SendMessageResult,
2227
SendMessagePayload,
2328
CreateDomainPayload,
@@ -55,6 +60,11 @@
5560
"AttachmentUrl",
5661
"DomainVerificationResult",
5762
"DeleteResult",
63+
"SearchResult",
64+
"SmsSendResult",
65+
"DeliveryMetrics",
66+
"DeliverySuppression",
67+
"DeliveryEvent",
5868
"SendMessageResult",
5969
"SendMessagePayload",
6070
"CreateDomainPayload",
@@ -74,4 +84,4 @@
7484
"WebhookVerificationError",
7585
]
7686

77-
__version__ = "0.2.0"
87+
__version__ = "0.3.0"

commune/async_client.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,19 @@ async def main():
3737
from commune.types import (
3838
Attachment,
3939
DeleteResult,
40+
DeliveryEvent,
41+
DeliveryMetrics,
42+
DeliverySuppression,
4043
DomainDnsRecord,
4144
DomainVerificationResult,
4245
CreateDomainPayload,
4346
CreateInboxPayload,
4447
UpdateInboxPayload,
4548
SetInboxWebhookPayload,
49+
SearchResult,
4650
SendMessagePayload,
4751
SendMessageResult,
52+
SmsSendResult,
4853
UploadAttachmentPayload,
4954
AttachmentUpload,
5055
AttachmentUrl,
@@ -766,6 +771,168 @@ async def url(self, attachment_id: str, *, expires_in: int = 3600) -> Attachment
766771
return AttachmentUrl.model_validate(data)
767772

768773

774+
class _AsyncSearch:
775+
"""Async full-text search across email threads."""
776+
777+
def __init__(self, http: AsyncHttpClient):
778+
self._http = http
779+
780+
async def threads(
781+
self,
782+
query: str,
783+
*,
784+
inbox_id: str | None = None,
785+
domain_id: str | None = None,
786+
limit: int = 20,
787+
) -> list[SearchResult]:
788+
"""Search across email threads by subject or content.
789+
790+
Args:
791+
query: Search query string.
792+
inbox_id: Scope search to a single inbox (recommended).
793+
domain_id: Scope search to a domain.
794+
limit: Max results (1–100, default 20).
795+
796+
Returns:
797+
List of SearchResult objects ordered by relevance.
798+
"""
799+
params: dict[str, Any] = {"q": query, "limit": limit}
800+
if inbox_id:
801+
params["inbox_id"] = inbox_id
802+
if domain_id:
803+
params["domain_id"] = domain_id
804+
data = await self._http.get("/v1/search/threads", params=params)
805+
if isinstance(data, list):
806+
return [SearchResult.model_validate(r) for r in data]
807+
return [SearchResult.model_validate(r) for r in (data or [])]
808+
809+
810+
class _AsyncSms:
811+
"""Async SMS sending."""
812+
813+
def __init__(self, http: AsyncHttpClient):
814+
self._http = http
815+
816+
async def send(
817+
self,
818+
*,
819+
to: str,
820+
body: str,
821+
phone_number_id: str | None = None,
822+
) -> SmsSendResult:
823+
"""Send an SMS message.
824+
825+
Args:
826+
to: Recipient phone number in E.164 format (e.g. "+15551234567").
827+
body: SMS message text.
828+
phone_number_id: Send from a specific provisioned number (optional).
829+
830+
Returns:
831+
SmsSendResult with .message_id, .status, .credits_charged.
832+
"""
833+
payload: dict[str, Any] = {"to": to, "body": body}
834+
if phone_number_id:
835+
payload["phone_number_id"] = phone_number_id
836+
data = await self._http.post("/v1/sms/send", json=payload)
837+
return SmsSendResult.model_validate(data)
838+
839+
840+
class _AsyncDelivery:
841+
"""Async deliverability monitoring."""
842+
843+
def __init__(self, http: AsyncHttpClient):
844+
self._http = http
845+
846+
async def metrics(
847+
self,
848+
*,
849+
inbox_id: str | None = None,
850+
domain_id: str | None = None,
851+
period: str = "7d",
852+
) -> DeliveryMetrics:
853+
"""Get email deliverability metrics.
854+
855+
Args:
856+
inbox_id: Filter metrics to a specific inbox.
857+
domain_id: Filter metrics to a domain.
858+
period: Time window — "24h", "7d" (default), or "30d".
859+
860+
Returns:
861+
DeliveryMetrics with sent, delivered, bounced, complained, failed counts
862+
and delivery_rate, bounce_rate, complaint_rate percentages.
863+
"""
864+
params: dict[str, Any] = {"period": period}
865+
if inbox_id:
866+
params["inbox_id"] = inbox_id
867+
if domain_id:
868+
params["domain_id"] = domain_id
869+
data = await self._http.get("/v1/delivery/metrics", params=params)
870+
return DeliveryMetrics.model_validate(data)
871+
872+
async def suppressions(
873+
self,
874+
*,
875+
inbox_id: str | None = None,
876+
domain_id: str | None = None,
877+
limit: int = 50,
878+
) -> list[DeliverySuppression]:
879+
"""List suppressed email addresses.
880+
881+
Args:
882+
inbox_id: Filter suppressions to a specific inbox.
883+
domain_id: Filter suppressions to a domain.
884+
limit: Max results (default 50).
885+
886+
Returns:
887+
List of DeliverySuppression objects with .email, .reason, .suppressed_at.
888+
"""
889+
params: dict[str, Any] = {"limit": limit}
890+
if inbox_id:
891+
params["inbox_id"] = inbox_id
892+
if domain_id:
893+
params["domain_id"] = domain_id
894+
data = await self._http.get("/v1/delivery/suppressions", params=params)
895+
if isinstance(data, list):
896+
return [DeliverySuppression.model_validate(s) for s in data]
897+
return [DeliverySuppression.model_validate(s) for s in (data or [])]
898+
899+
async def events(
900+
self,
901+
*,
902+
message_id: str | None = None,
903+
inbox_id: str | None = None,
904+
domain_id: str | None = None,
905+
event_type: str | None = None,
906+
limit: int = 50,
907+
) -> list[DeliveryEvent]:
908+
"""Get the delivery event log for messages.
909+
910+
Args:
911+
message_id: Filter events for a specific message.
912+
inbox_id: Filter events for all messages from an inbox.
913+
domain_id: Filter events for all messages from a domain.
914+
event_type: Filter by type: "sent", "delivered", "bounced",
915+
"complained", or "failed".
916+
limit: Max results (default 50).
917+
918+
Returns:
919+
List of DeliveryEvent objects with .event_type, .recipient, .timestamp.
920+
"""
921+
params: dict[str, Any] = {"limit": limit}
922+
if message_id:
923+
params["message_id"] = message_id
924+
if inbox_id:
925+
params["inbox_id"] = inbox_id
926+
if domain_id:
927+
params["domain_id"] = domain_id
928+
if event_type:
929+
params["event_type"] = event_type
930+
data = await self._http.get("/v1/delivery/events", params=params)
931+
if isinstance(data, list):
932+
return [DeliveryEvent.model_validate(e) for e in data]
933+
return [DeliveryEvent.model_validate(e) for e in (data or [])]
934+
935+
769936
class AsyncCommuneClient:
770937
"""Async Commune SDK client — email infrastructure for AI agents using asyncio.
771938
@@ -857,6 +1024,9 @@ def __init__(
8571024
self.threads = _AsyncThreads(self._http)
8581025
self.messages = _AsyncMessages(self._http)
8591026
self.attachments = _AsyncAttachments(self._http)
1027+
self.search = _AsyncSearch(self._http)
1028+
self.sms = _AsyncSms(self._http)
1029+
self.delivery = _AsyncDelivery(self._http)
8601030

8611031
async def close(self) -> None:
8621032
"""Close the underlying async HTTP connection pool.

0 commit comments

Comments
 (0)