diff --git a/src/devo_global_comms_python/client.py b/src/devo_global_comms_python/client.py index b268a30..d8695ab 100644 --- a/src/devo_global_comms_python/client.py +++ b/src/devo_global_comms_python/client.py @@ -4,6 +4,7 @@ from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry +from . import __version__ from .auth import APIKeyAuth from .exceptions import DevoAPIException, DevoAuthenticationException, DevoException, DevoMissingAPIKeyException from .resources.contacts import ContactsResource @@ -143,7 +144,7 @@ def request( # Prepare headers request_headers = { - "User-Agent": f"devo-python/{self.__class__.__module__.split('.')[0]}", + "User-Agent": f"devo-python-sdk/{__version__}", "Accept": "application/json", } if headers: diff --git a/src/devo_global_comms_python/models/sms.py b/src/devo_global_comms_python/models/sms.py index 2de0778..4f9a651 100644 --- a/src/devo_global_comms_python/models/sms.py +++ b/src/devo_global_comms_python/models/sms.py @@ -212,7 +212,7 @@ def __init__(self, data: List[Dict] = None, **kwargs): """Custom constructor to handle direct array response.""" if data is not None and isinstance(data, list): # Convert list of dicts to list of AvailableNumber objects - numbers = [AvailableNumber.parse_obj(item) for item in data] + numbers = [AvailableNumber.model_validate(item) for item in data] super().__init__(numbers=numbers, **kwargs) else: super().__init__(**kwargs) @@ -222,7 +222,7 @@ def __init__(self, data: List[Dict] = None, **kwargs): @classmethod def parse_from_list(cls, data: List[Dict]) -> "AvailableNumbersResponse": """Parse from direct array response.""" - numbers = [AvailableNumber.parse_obj(item) for item in data] + numbers = [AvailableNumber.model_validate(item) for item in data] return cls(numbers=numbers) diff --git a/src/devo_global_comms_python/resources/contacts.py b/src/devo_global_comms_python/resources/contacts.py index da88392..915489f 100644 --- a/src/devo_global_comms_python/resources/contacts.py +++ b/src/devo_global_comms_python/resources/contacts.py @@ -97,7 +97,7 @@ def list( from ..models.contacts import GetContactsSerializer - return GetContactsSerializer.parse_obj(response.json()) + return GetContactsSerializer.model_validate(response.json()) def create(self, contact_data: "CreateContactDto") -> "ContactSerializer": """ @@ -113,7 +113,7 @@ def create(self, contact_data: "CreateContactDto") -> "ContactSerializer": from ..models.contacts import ContactSerializer - return ContactSerializer.parse_obj(response.json()) + return ContactSerializer.model_validate(response.json()) def update(self, contact_id: str, contact_data: "UpdateContactDto") -> "ContactSerializer": """ @@ -132,7 +132,7 @@ def update(self, contact_id: str, contact_data: "UpdateContactDto") -> "ContactS from ..models.contacts import ContactSerializer - return ContactSerializer.parse_obj(response.json()) + return ContactSerializer.model_validate(response.json()) def delete_bulk(self, delete_data: "DeleteContactsDto", approve: Optional[str] = None) -> "ContactSerializer": """ @@ -153,7 +153,7 @@ def delete_bulk(self, delete_data: "DeleteContactsDto", approve: Optional[str] = from ..models.contacts import ContactSerializer - return ContactSerializer.parse_obj(response.json()) + return ContactSerializer.model_validate(response.json()) # Contact Group Management @@ -198,7 +198,7 @@ def import_from_csv( from ..models.contacts import CreateContactsFromCsvRespDto - return CreateContactsFromCsvRespDto.parse_obj(response.json()) + return CreateContactsFromCsvRespDto.model_validate(response.json()) # Custom Fields Management @@ -232,7 +232,7 @@ def list_custom_fields( from ..models.contacts import GetCustomFieldsSerializer - return GetCustomFieldsSerializer.parse_obj(response.json()) + return GetCustomFieldsSerializer.model_validate(response.json()) def create_custom_field(self, field_data: "CreateCustomFieldDto") -> "CustomFieldSerializer": """ @@ -248,7 +248,7 @@ def create_custom_field(self, field_data: "CreateCustomFieldDto") -> "CustomFiel from ..models.contacts import CustomFieldSerializer - return CustomFieldSerializer.parse_obj(response.json()) + return CustomFieldSerializer.model_validate(response.json()) def update_custom_field(self, field_id: str, field_data: "UpdateCustomFieldDto") -> None: """ diff --git a/src/devo_global_comms_python/resources/email.py b/src/devo_global_comms_python/resources/email.py index 32ed67c..1041d12 100644 --- a/src/devo_global_comms_python/resources/email.py +++ b/src/devo_global_comms_python/resources/email.py @@ -1,10 +1,10 @@ -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING from ..utils import validate_email, validate_required_string from .base import BaseResource if TYPE_CHECKING: - from ..models.email import EmailMessage, EmailSendResponse + from ..models.email import EmailSendResponse class EmailResource(BaseResource): @@ -60,151 +60,3 @@ def send_email( from ..models.email import EmailSendResponse return EmailSendResponse.model_validate(response.json()) - - def send( - self, - to: str, - subject: str, - body: str, - from_: Optional[str] = None, - html_body: Optional[str] = None, - cc: Optional[List[str]] = None, - bcc: Optional[List[str]] = None, - attachments: Optional[List[Dict[str, Any]]] = None, - reply_to: Optional[str] = None, - callback_url: Optional[str] = None, - metadata: Optional[Dict[str, Any]] = None, - ) -> "EmailMessage": - """ - Send an email message. - - Args: - to: The recipient's email address - subject: The email subject - body: The plain text email body - from_: The sender's email address (optional, uses account default) - html_body: The HTML email body (optional) - cc: List of CC email addresses (optional) - bcc: List of BCC email addresses (optional) - attachments: List of attachment objects (optional) - reply_to: Reply-to email address (optional) - callback_url: Webhook URL for delivery status (optional) - metadata: Custom metadata dictionary (optional) - - Returns: - EmailMessage: The sent message details - """ - # Validate inputs - to = validate_email(to) - subject = validate_required_string(subject, "subject") - body = validate_required_string(body, "body") - - if from_: - from_ = validate_email(from_) - if reply_to: - reply_to = validate_email(reply_to) - if cc: - cc = [validate_email(email) for email in cc] - if bcc: - bcc = [validate_email(email) for email in bcc] - - # Prepare request data - data = { - "to": to, - "subject": subject, - "body": body, - } - - if from_: - data["from"] = from_ - if html_body: - data["html_body"] = html_body - if cc: - data["cc"] = cc - if bcc: - data["bcc"] = bcc - if attachments: - data["attachments"] = attachments - if reply_to: - data["reply_to"] = reply_to - if callback_url: - data["callback_url"] = callback_url - if metadata: - data["metadata"] = metadata - - # Send request - response = self.client.post("email/messages", json=data) - - from ..models.email import EmailMessage - - return EmailMessage.parse_obj(response.json()) - - def get(self, message_id: str) -> "EmailMessage": - """ - Retrieve an email message by ID. - - Args: - message_id: The message ID - - Returns: - EmailMessage: The message details - """ - message_id = validate_required_string(message_id, "message_id") - - response = self.client.get(f"email/messages/{message_id}") - - from ..models.email import EmailMessage - - return EmailMessage.parse_obj(response.json()) - - def list( - self, - to: Optional[str] = None, - from_: Optional[str] = None, - subject: Optional[str] = None, - date_sent_after: Optional[str] = None, - date_sent_before: Optional[str] = None, - status: Optional[str] = None, - limit: int = 50, - offset: int = 0, - ) -> List["EmailMessage"]: - """ - List email messages with optional filtering. - - Args: - to: Filter by recipient email address - from_: Filter by sender email address - subject: Filter by subject (partial match) - date_sent_after: Filter messages sent after this date - date_sent_before: Filter messages sent before this date - status: Filter by message status - limit: Maximum number of messages to return (default: 50) - offset: Number of messages to skip (default: 0) - - Returns: - List[EmailMessage]: List of messages - """ - params = { - "limit": limit, - "offset": offset, - } - - if to: - params["to"] = validate_email(to) - if from_: - params["from"] = validate_email(from_) - if subject: - params["subject"] = subject - if date_sent_after: - params["date_sent_after"] = date_sent_after - if date_sent_before: - params["date_sent_before"] = date_sent_before - if status: - params["status"] = status - - response = self.client.get("email/messages", params=params) - data = response.json() - - from ..models.email import EmailMessage - - return [EmailMessage.parse_obj(item) for item in data.get("messages", [])] diff --git a/src/devo_global_comms_python/resources/messages.py b/src/devo_global_comms_python/resources/messages.py index 221e9e9..b629f64 100644 --- a/src/devo_global_comms_python/resources/messages.py +++ b/src/devo_global_comms_python/resources/messages.py @@ -93,7 +93,7 @@ def get(self, message_id: str) -> "Message": from ..models.messages import Message - return Message.parse_obj(response.json()) + return Message.model_validate(response.json()) def list( self, @@ -142,7 +142,7 @@ def list( from ..models.messages import Message - return [Message.parse_obj(item) for item in data.get("messages", [])] + return [Message.model_validate(item) for item in data.get("messages", [])] def get_delivery_status(self, message_id: str) -> Dict[str, Any]: """ @@ -173,4 +173,4 @@ def resend(self, message_id: str) -> "Message": from ..models.messages import Message - return Message.parse_obj(response.json()) + return Message.model_validate(response.json()) diff --git a/src/devo_global_comms_python/resources/rcs.py b/src/devo_global_comms_python/resources/rcs.py index 698f84e..5e4b012 100644 --- a/src/devo_global_comms_python/resources/rcs.py +++ b/src/devo_global_comms_python/resources/rcs.py @@ -17,7 +17,7 @@ def create_account(self, account_data: Dict[str, Any]) -> "RcsAccountSerializer" from ..models.rcs import RcsAccountSerializer - return RcsAccountSerializer.parse_obj(response.json()) + return RcsAccountSerializer.model_validate(response.json()) def get_accounts( self, @@ -65,7 +65,7 @@ def verify_account(self, verification_data: Dict[str, Any]) -> "SuccessSerialize from ..models.rcs import SuccessSerializer - return SuccessSerializer.parse_obj(response.json()) + return SuccessSerializer.model_validate(response.json()) def update_account(self, account_id: str, account_data: Dict[str, Any]) -> Dict[str, Any]: """Update RCS Account.""" @@ -86,7 +86,7 @@ def send_message(self, message_data: Dict[str, Any]) -> "RcsSendMessageSerialize from ..models.rcs import RcsSendMessageSerializer - return RcsSendMessageSerializer.parse_obj(response.json()) + return RcsSendMessageSerializer.model_validate(response.json()) def list_messages( self, @@ -113,7 +113,7 @@ def list_messages( from ..models.rcs import RcsSendMessageSerializer - return [RcsSendMessageSerializer.parse_obj(message) for message in response.json()] + return [RcsSendMessageSerializer.model_validate(message) for message in response.json()] # Template Management Endpoints def create_template(self, template_data: Dict[str, Any]) -> Dict[str, Any]: @@ -238,7 +238,7 @@ def send_text( from ..models.rcs import RCSMessage - return RCSMessage.parse_obj(response.json()) + return RCSMessage.model_validate(response.json()) def send_rich_card( self, @@ -277,7 +277,7 @@ def send_rich_card( from ..models.rcs import RCSMessage - return RCSMessage.parse_obj(response.json()) + return RCSMessage.model_validate(response.json()) def get(self, message_id: str) -> "RCSMessage": """Retrieve an RCS message by ID.""" @@ -286,4 +286,4 @@ def get(self, message_id: str) -> "RCSMessage": from ..models.rcs import RCSMessage - return RCSMessage.parse_obj(response.json()) + return RCSMessage.model_validate(response.json()) diff --git a/src/devo_global_comms_python/resources/sms.py b/src/devo_global_comms_python/resources/sms.py index fc5f36a..9907046 100644 --- a/src/devo_global_comms_python/resources/sms.py +++ b/src/devo_global_comms_python/resources/sms.py @@ -1,9 +1,3 @@ -""" -SMS resource for the Devo Global Communications API. - -Implements SMS API endpoints for sending messages and managing phone numbers. -""" - import logging from datetime import datetime from typing import TYPE_CHECKING, List, Optional @@ -113,7 +107,7 @@ def send_sms( # Parse response according to API spec from ..models.sms import SMSQuickSendResponse - result = SMSQuickSendResponse.parse_obj(response.json()) + result = SMSQuickSendResponse.model_validate(response.json()) logger.info(f"SMS sent successfully with ID: {result.id}") return result @@ -142,7 +136,7 @@ def get_senders(self) -> "SendersListResponse": # Parse response according to API spec from ..models.sms import SendersListResponse - result = SendersListResponse.parse_obj(response.json()) + result = SendersListResponse.model_validate(response.json()) logger.info(f"Retrieved {len(result.senders)} senders") return result @@ -219,7 +213,7 @@ def buy_number( # Parse response according to API spec from ..models.sms import NumberPurchaseResponse - result = NumberPurchaseResponse.parse_obj(response.json()) + result = NumberPurchaseResponse.model_validate(response.json()) feature_count = len(result.features) if result.features else 0 logger.info(f"Number purchased successfully with {feature_count} features") @@ -291,7 +285,7 @@ def get_available_numbers( result = AvailableNumbersResponse.parse_from_list(response_data) else: # Fallback to normal parsing if API changes - result = AvailableNumbersResponse.parse_obj(response_data) + result = AvailableNumbersResponse.model_validate(response_data) logger.info(f"Retrieved {len(result.numbers)} available numbers") @@ -323,61 +317,3 @@ def send(self, to: str, body: str, from_: Optional[str] = None, **kwargs) -> "SM sender=from_, hirvalidation=kwargs.get("hirvalidation", True), ) - - def get(self, message_id: str) -> dict: - """ - Legacy method for getting message details (backward compatibility). - - Args: - message_id: The message ID - - Returns: - dict: Message details - - Note: - This method provides basic compatibility but may not return - the full SMSMessage model structure. - """ - # This would need to be implemented based on a separate API endpoint - # if available, or could be removed if not supported by the API - raise NotImplementedError( - "Message retrieval by ID is not supported by the current API. " - "Use send_sms() to get message details upon sending." - ) - - def list(self, **kwargs) -> List[dict]: - """ - Legacy method for listing messages (backward compatibility). - - Returns: - List[dict]: List of messages - - Note: - This method provides basic compatibility but may not return - the full message structure. Consider using get_senders() or - get_available_numbers() for current functionality. - """ - # This would need to be implemented based on a separate API endpoint - # if available, or could be removed if not supported by the API - raise NotImplementedError( - "Message listing is not supported by the current API. " - "Use get_senders() or get_available_numbers() instead." - ) - - def cancel(self, message_id: str) -> dict: - """ - Legacy method for canceling messages (backward compatibility). - - Args: - message_id: The message ID to cancel - - Returns: - dict: Cancellation result - - Note: - This method provides basic compatibility but may not be - supported by the current API. - """ - # This would need to be implemented based on a separate API endpoint - # if available, or could be removed if not supported by the API - raise NotImplementedError("Message cancellation is not supported by the current API.") diff --git a/src/devo_global_comms_python/resources/whatsapp.py b/src/devo_global_comms_python/resources/whatsapp.py index b4fb0cc..fd4598d 100644 --- a/src/devo_global_comms_python/resources/whatsapp.py +++ b/src/devo_global_comms_python/resources/whatsapp.py @@ -435,7 +435,7 @@ def send_text( from ..models.whatsapp import WhatsAppMessage - return WhatsAppMessage.parse_obj(response.json()) + return WhatsAppMessage.model_validate(response.json()) def send_template( self, @@ -476,7 +476,7 @@ def send_template( from ..models.whatsapp import WhatsAppMessage - return WhatsAppMessage.parse_obj(response.json()) + return WhatsAppMessage.model_validate(response.json()) def get(self, message_id: str) -> "WhatsAppMessage": """Retrieve a WhatsApp message by ID.""" @@ -485,4 +485,4 @@ def get(self, message_id: str) -> "WhatsAppMessage": from ..models.whatsapp import WhatsAppMessage - return WhatsAppMessage.parse_obj(response.json()) + return WhatsAppMessage.model_validate(response.json()) diff --git a/tests/test_sms.py b/tests/test_sms.py index af61491..86efe8c 100644 --- a/tests/test_sms.py +++ b/tests/test_sms.py @@ -335,12 +335,8 @@ def test_legacy_send_without_sender_fails(self, sms_resource, test_phone_number) sms_resource.send(to=test_phone_number, body="Hello, World!") def test_legacy_methods_not_implemented(self, sms_resource): - """Test that unsupported legacy methods raise NotImplementedError.""" - with pytest.raises(NotImplementedError): - sms_resource.get("msg_123") - - with pytest.raises(NotImplementedError): - sms_resource.list() - - with pytest.raises(NotImplementedError): - sms_resource.cancel("msg_123") + """Test that unsupported legacy methods don't exist.""" + # These methods were completely removed for cleaner API + assert not hasattr(sms_resource, "get") + assert not hasattr(sms_resource, "list") + assert not hasattr(sms_resource, "cancel")