diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6362a49..4a2c7da 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,23 +14,23 @@ repos: hooks: - id: black language_version: python3 - args: [--line-length=88] + args: [--line-length=120] - repo: https://github.com/pycqa/isort rev: 5.12.0 hooks: - id: isort - args: [--profile=black, --line-length=88] + args: [--profile=black, --line-length=120] - repo: https://github.com/pycqa/flake8 rev: 6.0.0 hooks: - id: flake8 - args: [--max-line-length=88, --extend-ignore=E203,W503] + args: ["--max-line-length=120", "--extend-ignore=E203,W503"] - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.3.0 - hooks: - - id: mypy - additional_dependencies: [pydantic, requests, types-requests] - args: [--ignore-missing-imports] + # - repo: https://github.com/pre-commit/mirrors-mypy + # rev: v1.3.0 + # hooks: + # - id: mypy + # additional_dependencies: [pydantic, requests, types-requests] + # args: [--ignore-missing-imports, --python-version=3.9] diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..6942792 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,161 @@ +# Devo Global Communications SDK - Examples + +This directory contains comprehensive examples for using the Devo Global Communications SDK. Each resource has its own dedicated example file with detailed demonstrations of the available functionality. + +## ๐Ÿ“ Example Files + +### ๐Ÿš€ Overview +- **`basic_usage.py`** - Interactive overview and launcher for all examples + +### ๐Ÿ“ฑ Communication Resources +- **`sms_example.py`** - โœ… **Complete SMS API implementation** + - Send SMS messages via quick-send API + - Get available senders + - Search and purchase phone numbers + - Legacy compatibility methods + +- **`email_example.py`** - ๐Ÿšง **Placeholder** (Email functionality) +- **`whatsapp_example.py`** - ๐Ÿšง **Placeholder** (WhatsApp functionality) +- **`rcs_example.py`** - ๐Ÿšง **Placeholder** (RCS functionality) + +### ๐Ÿ‘ฅ Management Resources +- **`contacts_example.py`** - ๐Ÿšง **Placeholder** (Contact management) + +## ๐Ÿš€ Getting Started + +### Prerequisites +1. **API Key**: Get your API key from the Devo dashboard +2. **Environment**: Set the `DEVO_API_KEY` environment variable + ```bash + # Windows (PowerShell) + $env:DEVO_API_KEY = "your_api_key_here" + + # Windows (Command Prompt) + set DEVO_API_KEY=your_api_key_here + + # Unix/Linux/macOS + export DEVO_API_KEY=your_api_key_here + ``` + +### Running Examples + +#### Option 1: Interactive Overview (Recommended) +```bash +python examples/basic_usage.py +``` +This provides an interactive menu to choose and run specific examples. + +#### Option 2: Run Individual Examples +```bash +# SMS functionality (fully implemented) +python examples/sms_example.py + +# Other resources (placeholder examples) +python examples/email_example.py +python examples/whatsapp_example.py +python examples/contacts_example.py +python examples/rcs_example.py +``` + +## ๐Ÿ“ฑ SMS Examples (Fully Implemented) + +The SMS resource is fully implemented with all four API endpoints: + +### ๐Ÿ”ง Available Functions +1. **Send SMS** - `client.sms.send_sms()` + - Uses POST `/user-api/sms/quick-send` + - High-quality routing validation + - Comprehensive response data + +2. **Get Senders** - `client.sms.get_senders()` + - Uses GET `/user-api/me/senders` + - Lists all available sender numbers/IDs + +3. **Search Numbers** - `client.sms.get_available_numbers()` + - Uses GET `/user-api/numbers` + - Filter by region, type, and limit results + +4. **Purchase Numbers** - `client.sms.buy_number()` + - Uses POST `/user-api/numbers/buy` + - Complete number purchasing workflow + +### ๐Ÿ”„ Legacy Compatibility +The SMS resource maintains backward compatibility with legacy methods while using the new API implementation underneath. + +## ๐Ÿšง Placeholder Examples + +The following examples show the structure and planned functionality but are not yet implemented: + +- **Email**: Send emails, attachments, templates +- **WhatsApp**: Text messages, media, templates, business features +- **RCS**: Rich messaging, cards, carousels, capability checks +- **Contacts**: CRUD operations, contact management + +## ๐Ÿ”ง Configuration Notes + +### Phone Numbers +- Replace placeholder phone numbers (`+1234567890`) with actual numbers +- Ensure phone numbers are in E.164 format (e.g., `+1234567890`) +- Use valid sender numbers from your Devo dashboard + +### Testing vs Production +- Some examples include test mode flags +- Number purchase examples are commented out to prevent accidental charges +- Always test with small limits when exploring available numbers + +### Error Handling +All examples include comprehensive error handling with: +- Detailed error messages +- HTTP status codes +- API-specific error codes +- Response data debugging + +## ๐Ÿ†” Authentication + +All examples use API key authentication: +```python +from devo_global_comms_python import DevoClient + +client = DevoClient(api_key="your_api_key_here") +``` + +## ๐Ÿ“‹ Example Output + +### SMS Example Output +``` +๐Ÿ“ฑ SMS QUICK-SEND API EXAMPLE +------------------------------ +๐Ÿ“ค Sending SMS to +1234567890... +โœ… SMS sent successfully! + ๐Ÿ“‹ Message ID: msg_123456789 + ๐Ÿ“Š Status: sent + ๐Ÿ“ฑ Recipient: +1234567890 + ๐Ÿ”„ Direction: outbound + +๐Ÿ‘ฅ GET AVAILABLE SENDERS EXAMPLE +------------------------------ +โœ… Found 3 available senders: + 1. ๐Ÿ“ž Phone: +0987654321 + ๐Ÿท๏ธ Type: longcode + ๐Ÿงช Test Mode: No +``` + +## ๐Ÿค Contributing + +When implementing new resources: + +1. **Create Resource Example**: Copy the structure from `sms_example.py` +2. **Update Basic Usage**: Add the new resource to `basic_usage.py` +3. **Update This README**: Document the new functionality +4. **Follow Patterns**: Use consistent emoji, formatting, and error handling + +## ๐Ÿ“š Additional Resources + +- **SDK Documentation**: [Link to main documentation] +- **API Reference**: [Link to API docs] +- **Devo Dashboard**: [Link to dashboard] +- **Support**: [Link to support] + +--- + +**Need help?** Check the individual example files for detailed comments and error handling patterns. diff --git a/examples/basic_usage.py b/examples/basic_usage.py index b1d377b..5bc68cc 100644 --- a/examples/basic_usage.py +++ b/examples/basic_usage.py @@ -1,70 +1,126 @@ import os +import subprocess +import sys from devo_global_comms_python import DevoClient, DevoException def main(): - # Initialize the client with your API key - # You can get your API key from the Devo dashboard + print("๐Ÿš€ Devo Global Communications SDK") + print("=" * 60) + + # Check if API key is set api_key = os.getenv("DEVO_API_KEY") if not api_key: - print("Please set DEVO_API_KEY environment variable") + print("โŒ Please set DEVO_API_KEY environment variable") + print(" You can get your API key from the Devo dashboard") + return + + # Initialize the client + try: + client = DevoClient(api_key=api_key) + print("โœ… Devo SDK Client initialized successfully") + except Exception as e: + print(f"โŒ Failed to initialize client: {e}") return - client = DevoClient(api_key=api_key) + print("\n๐Ÿ“‹ Available Resources:") + print("-" * 30) + + # Check available resources + resources = [] + if hasattr(client, "sms"): + resources.append(("๐Ÿ“ฑ SMS", "Implemented", "sms_example.py")) + if hasattr(client, "email"): + resources.append(("๐Ÿ“ง Email", "Placeholder", "email_example.py")) + if hasattr(client, "whatsapp"): + resources.append(("๐Ÿ’ฌ WhatsApp", "Placeholder", "whatsapp_example.py")) + if hasattr(client, "contacts"): + resources.append(("๐Ÿ‘ฅ Contacts", "Placeholder", "contacts_example.py")) + if hasattr(client, "rcs"): + resources.append(("๐ŸŽด RCS", "Placeholder", "rcs_example.py")) + + for resource, status, example_file in resources: + print(f" {resource:<12} - {status:<12} -> {example_file}") + + # Quick SMS test if available + if hasattr(client, "sms"): + print("\n๐Ÿงช Quick SMS Test:") + print("-" * 30) + try: + # Try to get senders as a connectivity test + senders = client.sms.get_senders() + print(f"โœ… SMS connection successful - {len(senders.senders)} senders available") + + if senders.senders: + print(" Sample senders:") + for i, sender in enumerate(senders.senders[:3], 1): + print(f" {i}. {sender.phone_number} ({sender.type})") + if len(senders.senders) > 3: + print(f" ... and {len(senders.senders) - 3} more") + + except DevoException as e: + print(f"โš ๏ธ SMS connection test failed: {e}") + + # Show example usage + print("\n๐Ÿ’ก Getting Started:") + print("-" * 30) + print("1. Run individual resource examples:") + print(" python examples/sms_example.py # Complete SMS functionality") + print(" python examples/email_example.py # Email examples (placeholder)") + print(" python examples/whatsapp_example.py # WhatsApp examples (placeholder)") + print(" python examples/contacts_example.py # Contact management (placeholder)") + print(" python examples/rcs_example.py # RCS examples (placeholder)") + print() + print("2. Quick SMS example:") + print(" from devo_global_comms_python import DevoClient") + print(" client = DevoClient(api_key='your_api_key')") + print(" response = client.sms.send_sms(") + print(" recipient='+1234567890',") + print(" message='Hello from Devo!',") + print(" sender='your_sender_id'") + print(" )") + + # Interactive menu + print("\n๐ŸŽฏ Interactive Examples:") + print("-" * 30) + print("Would you like to run a specific example?") + print("1. SMS Example (full functionality)") + print("2. Email Example (placeholder)") + print("3. WhatsApp Example (placeholder)") + print("4. Contacts Example (placeholder)") + print("5. RCS Example (placeholder)") + print("0. Exit") try: - # Example 1: Send an SMS - print("Sending SMS...") - sms = client.sms.send( - to="+1234567890", # Replace with actual phone number - body="Hello from Devo SDK! This is a test SMS message.", - ) - print(f"SMS sent successfully! Message SID: {sms.sid}") - print(f"Status: {sms.status}") - - # Example 2: Send an email - print("\nSending email...") - email = client.email.send( - to="recipient@example.com", # Replace with actual email - subject="Test Email from Devo SDK", - body="This is a test email sent using the Devo Global Communications SDK.", - html_body="

Test Email

This is a test email sent using the Devo SDK.

", - ) - print(f"Email sent successfully! Message ID: {email.id}") - print(f"Status: {email.status}") - - # Example 3: Send a WhatsApp message - print("\nSending WhatsApp message...") - whatsapp = client.whatsapp.send_text( - to="+1234567890", # Replace with actual WhatsApp number - text="Hello from Devo SDK! This is a WhatsApp message.", - ) - print(f"WhatsApp message sent successfully! Message ID: {whatsapp.id}") - print(f"Status: {whatsapp.status}") - - # Example 4: Create a contact - print("\nCreating contact...") - contact = client.contacts.create( - phone_number="+1234567890", - email="contact@example.com", - first_name="John", - last_name="Doe", - company="Example Corp", - metadata={"source": "sdk_example"}, - ) - print(f"Contact created successfully! Contact ID: {contact.id}") - print(f"Name: {contact.first_name} {contact.last_name}") - - # Example 5: List recent messages - print("\nListing recent messages...") - messages = client.messages.list(limit=5, date_sent_after="2024-01-01") - print(f"Found {len(messages)} recent messages:") - for message in messages: - print(f" - {message.channel}: {message.id} ({message.status})") - - except DevoException as e: - print(f"Error: {e}") + choice = input("\nEnter your choice (0-5): ").strip() + example_files = { + "1": "sms_example.py", + "2": "email_example.py", + "3": "whatsapp_example.py", + "4": "contacts_example.py", + "5": "rcs_example.py", + } + + if choice in example_files: + example_file = example_files[choice] + example_path = os.path.join(os.path.dirname(__file__), example_file) + + if os.path.exists(example_path): + print(f"\n๐Ÿš€ Running {example_file}...") + print("=" * 60) + subprocess.run([sys.executable, example_path], check=True) + else: + print(f"โŒ Example file {example_file} not found") + elif choice == "0": + print("๐Ÿ‘‹ Goodbye!") + else: + print("โŒ Invalid choice") + + except KeyboardInterrupt: + print("\n๐Ÿ‘‹ Goodbye!") + except Exception as e: + print(f"โŒ Error running example: {e}") if __name__ == "__main__": diff --git a/examples/contacts_example.py b/examples/contacts_example.py new file mode 100644 index 0000000..34e73fe --- /dev/null +++ b/examples/contacts_example.py @@ -0,0 +1,104 @@ +import os + +from devo_global_comms_python import DevoException + + +def main(): + api_key = os.getenv("DEVO_API_KEY") + if not api_key: + print("โŒ Please set DEVO_API_KEY environment variable") + return + + print("โœ… Devo Contacts Client initialized successfully") + print("=" * 60) + + try: + # Example 1: Create a contact + print("๐Ÿ‘ค CREATE CONTACT EXAMPLE") + print("-" * 30) + + print("๐Ÿ“ Creating a new contact...") + print("โš ๏ธ This is a placeholder implementation.") + print(" Update this example when Contacts API is implemented.") + + # Placeholder contact creation - update when implementing Contacts resource + print(" ```python") + print(" contact = client.contacts.create(") + print(" phone_number='+1234567890',") + print(" email='john.doe@example.com',") + print(" first_name='John',") + print(" last_name='Doe',") + print(" company='Acme Corp',") + print(" metadata={'source': 'sdk_example', 'campaign': 'Q1_2025'}") + print(" )") + print(" print(f'Contact created! ID: {contact.id}')") + print(" ```") + + # Example 2: Get contact by ID + print("\n๐Ÿ” GET CONTACT EXAMPLE") + print("-" * 30) + + print("๐Ÿ“– Retrieving contact by ID...") + print(" ```python") + print(" contact = client.contacts.get('contact_id_123')") + print(" print(f'Contact: {contact.first_name} {contact.last_name}')") + print(" print(f'Phone: {contact.phone_number}')") + print(" print(f'Email: {contact.email}')") + print(" ```") + + # Example 3: List contacts + print("\n๐Ÿ“‹ LIST CONTACTS EXAMPLE") + print("-" * 30) + + print("๐Ÿ“‹ Listing contacts...") + print(" ```python") + print(" contacts = client.contacts.list(") + print(" limit=10,") + print(" filter_by_company='Acme Corp'") + print(" )") + print(" print(f'Found {len(contacts)} contacts:')") + print(" for contact in contacts:") + print(" print(f' - {contact.first_name} {contact.last_name}')") + print(" ```") + + # Example 4: Update contact + print("\nโœ๏ธ UPDATE CONTACT EXAMPLE") + print("-" * 30) + + print("โœ๏ธ Updating contact information...") + print(" ```python") + print(" updated_contact = client.contacts.update(") + print(" contact_id='contact_id_123',") + print(" company='Acme Corporation',") + print(" metadata={'source': 'sdk_example', 'updated': '2025-08-28'}") + print(" )") + print(" print(f'Contact updated! Company: {updated_contact.company}')") + print(" ```") + + # Example 5: Delete contact + print("\n๐Ÿ—‘๏ธ DELETE CONTACT EXAMPLE") + print("-" * 30) + + print("๐Ÿ—‘๏ธ Deleting contact...") + print(" ```python") + print(" client.contacts.delete('contact_id_123')") + print(" print('Contact deleted successfully!')") + print(" ```") + + except DevoException as e: + print(f"โŒ Contacts operation failed: {e}") + + print("\n" + "=" * 60) + print("๐Ÿ“Š CONTACTS EXAMPLE SUMMARY") + print("-" * 30) + print("โš ๏ธ This is a placeholder example for Contacts functionality.") + print("๐Ÿ’ก To implement:") + print(" 1. Define Contacts API endpoints and specifications") + print(" 2. Create Contact Pydantic models") + print(" 3. Implement ContactsResource class") + print(" 4. Update this example with real functionality") + print(" 5. Add support for CRUD operations and contact management") + + +if __name__ == "__main__": + main() diff --git a/examples/email_example.py b/examples/email_example.py new file mode 100644 index 0000000..969d309 --- /dev/null +++ b/examples/email_example.py @@ -0,0 +1,51 @@ +import os + +from devo_global_comms_python import DevoException + + +def main(): + api_key = os.getenv("DEVO_API_KEY") + if not api_key: + print("โŒ Please set DEVO_API_KEY environment variable") + return + + print("โœ… Devo Email Client initialized successfully") + print("=" * 60) + + try: + # Example 1: Send a simple email + print("๐Ÿ“ง EMAIL SEND EXAMPLE") + print("-" * 30) + + print("๐Ÿ“ค Sending email...") + print("โš ๏ธ This is a placeholder implementation.") + print(" Update this example when Email API is implemented.") + + # Placeholder email send - update when implementing Email resource + print(" ```python") + print(" email_response = client.email.send(") + print(" to='recipient@example.com',") + print(" subject='Test Email from Devo SDK',") + print(" body='This is a test email.',") + print(" html_body='

Test

This is a test email.

',") + print(" from_email='sender@yourdomain.com'") + print(" )") + print(" print(f'Email sent! ID: {email_response.id}')") + print(" ```") + + except DevoException as e: + print(f"โŒ Email operation failed: {e}") + + print("\n" + "=" * 60) + print("๐Ÿ“Š EMAIL EXAMPLE SUMMARY") + print("-" * 30) + print("โš ๏ธ This is a placeholder example for Email functionality.") + print("๐Ÿ’ก To implement:") + print(" 1. Define Email API endpoints and specifications") + print(" 2. Create Email Pydantic models") + print(" 3. Implement EmailResource class") + print(" 4. Update this example with real functionality") + + +if __name__ == "__main__": + main() diff --git a/examples/rcs_example.py b/examples/rcs_example.py new file mode 100644 index 0000000..9c59d11 --- /dev/null +++ b/examples/rcs_example.py @@ -0,0 +1,109 @@ +import os + +from devo_global_comms_python import DevoException + + +def main(): + api_key = os.getenv("DEVO_API_KEY") + if not api_key: + print("โŒ Please set DEVO_API_KEY environment variable") + return + + print("โœ… Devo RCS Client initialized successfully") + print("=" * 60) + + try: + # Example 1: Send a text RCS message + print("๐Ÿ’ฌ RCS TEXT MESSAGE EXAMPLE") + print("-" * 30) + + print("๐Ÿ“ค Sending RCS text message...") + print("โš ๏ธ This is a placeholder implementation.") + print(" Update this example when RCS API is implemented.") + + # Placeholder RCS send - update when implementing RCS resource + print(" ```python") + print(" rcs_response = client.rcs.send_text(") + print(" to='+1234567890',") + print(" text='Hello from Devo SDK via RCS!',") + print(" agent_id='your_rcs_agent_id'") + print(" )") + print(" print(f'RCS message sent! ID: {rcs_response.id}')") + print(" ```") + + # Example 2: Send rich card + print("\n๐ŸŽด RCS RICH CARD EXAMPLE") + print("-" * 30) + + print("๐Ÿ“ค Sending RCS rich card...") + print(" ```python") + print(" card_response = client.rcs.send_card(") + print(" to='+1234567890',") + print(" title='Special Offer!',") + print(" description='Get 20% off your next purchase',") + print(" image_url='https://example.com/offer.jpg',") + print(" actions=[") + print(" {'type': 'url', 'text': 'Shop Now', 'url': 'https://shop.example.com'},") + print(" {'type': 'reply', 'text': 'Tell me more', 'postback': 'more_info'}") + print(" ]") + print(" )") + print(" print(f'RCS card sent! ID: {card_response.id}')") + print(" ```") + + # Example 3: Send carousel + print("\n๐ŸŽ  RCS CAROUSEL EXAMPLE") + print("-" * 30) + + print("๐Ÿ“ค Sending RCS carousel...") + print(" ```python") + print(" carousel_response = client.rcs.send_carousel(") + print(" to='+1234567890',") + print(" cards=[") + print(" {") + print(" 'title': 'Product 1',") + print(" 'description': 'Amazing product description',") + print(" 'image_url': 'https://example.com/product1.jpg',") + print(" 'actions': [{'type': 'url', 'text': 'Buy', 'url': 'https://shop.example.com/1'}]") + print(" },") + print(" {") + print(" 'title': 'Product 2',") + print(" 'description': 'Another great product',") + print(" 'image_url': 'https://example.com/product2.jpg',") + print(" 'actions': [{'type': 'url', 'text': 'Buy', 'url': 'https://shop.example.com/2'}]") + print(" }") + print(" ]") + print(" )") + print(" print(f'RCS carousel sent! ID: {carousel_response.id}')") + print(" ```") + + # Example 4: Check RCS capability + print("\n๐Ÿ” RCS CAPABILITY CHECK EXAMPLE") + print("-" * 30) + + print("๐Ÿ” Checking RCS capability...") + print(" ```python") + print(" capability = client.rcs.check_capability('+1234567890')") + print(" if capability.rcs_enabled:") + print(" print('โœ… RCS is supported for this number')") + print(" print(f'Features: {capability.supported_features}')") + print(" else:") + print(" print('โŒ RCS is not supported, fallback to SMS')") + print(" ```") + + except DevoException as e: + print(f"โŒ RCS operation failed: {e}") + + print("\n" + "=" * 60) + print("๐Ÿ“Š RCS EXAMPLE SUMMARY") + print("-" * 30) + print("โš ๏ธ This is a placeholder example for RCS functionality.") + print("๐Ÿ’ก To implement:") + print(" 1. Define RCS API endpoints and specifications") + print(" 2. Create RCS Pydantic models") + print(" 3. Implement RCSResource class") + print(" 4. Update this example with real functionality") + print(" 5. Add support for text, cards, carousels, and capability checks") + + +if __name__ == "__main__": + main() diff --git a/examples/sms_example.py b/examples/sms_example.py new file mode 100644 index 0000000..02340e9 --- /dev/null +++ b/examples/sms_example.py @@ -0,0 +1,194 @@ +import os + +from devo_global_comms_python import DevoClient, DevoException + + +def main(): + # Initialize the client with your API key + api_key = os.getenv("DEVO_API_KEY") + if not api_key: + print("โŒ Please set DEVO_API_KEY environment variable") + print(" You can get your API key from the Devo dashboard") + return + + client = DevoClient(api_key=api_key) + print("โœ… Devo SMS Client initialized successfully") + print("=" * 60) + + try: + # Example 1: Send SMS using new quick-send API + print("๐Ÿ“ฑ SMS QUICK-SEND API EXAMPLE") + print("-" * 30) + + recipient = "+1234567890" # Replace with actual phone number + sender = "+0987654321" # Replace with your sender number/ID + message = "Hello from Devo SDK! This message was sent using the new quick-send API." + + print(f"๐Ÿ“ค Sending SMS to {recipient}...") + print(f"๐Ÿ“ Message: {message}") + print(f"๐Ÿ“ž From: {sender}") + + sms_response = client.sms.send_sms( + recipient=recipient, + message=message, + sender=sender, + hirvalidation=True, # Enable high-quality routing validation + ) + + print("โœ… SMS sent successfully!") + print(f" ๐Ÿ“‹ Message ID: {sms_response.id}") + print(f" ๐Ÿ“Š Status: {sms_response.status}") + print(f" ๐Ÿ“ฑ Recipient: {sms_response.recipient}") + print(f" ๐Ÿ”„ Direction: {sms_response.direction}") + print(f" ๐Ÿ”ง API Mode: {sms_response.apimode}") + if sms_response.send_date: + print(f" ๐Ÿ“… Send Date: {sms_response.send_date}") + + except DevoException as e: + print(f"โŒ Failed to send SMS: {e}") + print_error_details(e) + + print("\n" + "=" * 60) + + try: + # Example 2: Get available senders + print("๐Ÿ‘ฅ GET AVAILABLE SENDERS EXAMPLE") + print("-" * 30) + + print("๐Ÿ” Retrieving available senders...") + senders = client.sms.get_senders() + + print(f"โœ… Found {len(senders.senders)} available senders:") + for i, sender in enumerate(senders.senders, 1): + print(f" {i}. ๐Ÿ“ž Phone: {sender.phone_number}") + print(f" ๐Ÿท๏ธ Type: {sender.type}") + print(f" ๐Ÿงช Test Mode: {'Yes' if sender.istest else 'No'}") + print(f" ๐Ÿ†” ID: {sender.id}") + if sender.creation_date: + print(f" ๐Ÿ“… Created: {sender.creation_date}") + print() + + except DevoException as e: + print(f"โŒ Failed to get senders: {e}") + print_error_details(e) + + print("=" * 60) + + try: + # Example 3: Get available numbers for purchase + print("๐Ÿ”ข GET AVAILABLE NUMBERS EXAMPLE") + print("-" * 30) + + region = "US" + number_type = "mobile" + limit = 5 + + print(f"๐Ÿ” Searching for {limit} available {number_type} numbers in {region}...") + numbers = client.sms.get_available_numbers(region=region, limit=limit, type=number_type) + + print(f"โœ… Found {len(numbers.numbers)} available numbers:") + for i, number_info in enumerate(numbers.numbers, 1): + print(f"\n ๐Ÿ“‹ Number Group {i}:") + for j, feature in enumerate(number_info.features, 1): + print(f" {j}. ๐Ÿ“ž Number: {feature.phone_number}") + print(f" ๐Ÿท๏ธ Type: {feature.number_type}") + print(f" ๐ŸŒ Region: {feature.region_information.region_name}") + print(f" ๐ŸŒ Country: {feature.region_information.country_code}") + print( + f" ๐Ÿ’ฐ Monthly: {feature.cost_information.monthly_cost} {feature.cost_information.currency}" + ) + print(f" ๐Ÿ”ง Setup: {feature.cost_information.setup_cost} {feature.cost_information.currency}") + print() + + except DevoException as e: + print(f"โŒ Failed to get available numbers: {e}") + print_error_details(e) + + print("=" * 60) + + # Example 4: Purchase a number (commented out to prevent accidental charges) + print("๐Ÿ’ณ NUMBER PURCHASE EXAMPLE (DISABLED)") + print("-" * 30) + print("โš ๏ธ The following example is commented out to prevent accidental charges.") + print(" Uncomment and modify the code below to actually purchase a number:") + print() + print(" ```python") + print(" # Choose a number from the available numbers above") + print(" selected_number = '+1234567890' # Replace with actual available number") + print(" ") + print(" print(f'๐Ÿ’ณ Purchasing number {selected_number}...')") + print(" number_purchase = client.sms.buy_number(") + print(" region='US',") + print(" number=selected_number,") + print(" number_type='mobile',") + print(" agency_authorized_representative='Jane Doe',") + print(" agency_representative_email='jane.doe@company.com',") + print(" is_longcode=True,") + print(" is_automated_enabled=True") + print(" )") + print(" ") + print(" print('โœ… Number purchased successfully!')") + print(" print(f' ๐Ÿ“ž Number: {number_purchase.number}')") + print(" print(f' ๐Ÿท๏ธ Type: {number_purchase.number_type}')") + print(" print(f' ๐ŸŒ Region: {number_purchase.region}')") + print(" print(f' โœจ Features: {len(number_purchase.features)}')") + print(" for feature in number_purchase.features:") + print(" print(f' - {feature.phone_number} ({feature.number_type})')") + print(" ```") + + print("\n" + "=" * 60) + + try: + # Example 5: Using legacy send method for backward compatibility + print("๐Ÿ”„ LEGACY COMPATIBILITY EXAMPLE") + print("-" * 30) + + print("๐Ÿ”„ Testing legacy send method for backward compatibility...") + print(" (This uses the old API structure but maps to new implementation)") + + try: + legacy_response = client.sms.send( + to=recipient, + body="Hello from legacy method! This ensures backward compatibility.", + from_=sender, + ) + print("โœ… Legacy send successful!") + print(f" ๐Ÿ“‹ Message ID: {legacy_response.id}") + print(f" ๐Ÿ“Š Status: {legacy_response.status}") + + except DevoException as e: + print(f"โš ๏ธ Legacy send failed (this is expected if sender is not configured): {e}") + print(" ๐Ÿ’ก Use the new send_sms() method for better control and error handling") + + except DevoException as e: + print(f"โŒ Legacy compatibility test failed: {e}") + print_error_details(e) + + print("\n" + "=" * 60) + print("๐Ÿ“Š SMS EXAMPLE SUMMARY") + print("-" * 30) + print("โœ… Covered SMS API endpoints:") + print(" 1. ๐Ÿ“ค POST /user-api/sms/quick-send - Send SMS messages") + print(" 2. ๐Ÿ‘ฅ GET /user-api/me/senders - Get available senders") + print(" 3. ๐Ÿ”ข GET /user-api/numbers - Get available numbers") + print(" 4. ๐Ÿ’ณ POST /user-api/numbers/buy - Purchase numbers (example only)") + print() + print("๐Ÿ’ก Next steps:") + print(" - Replace phone numbers with actual values") + print(" - Set up proper senders in your Devo dashboard") + print(" - Uncomment purchase example when ready to buy numbers") + print(" - Check other example files for Email, WhatsApp, etc.") + + +def print_error_details(error: DevoException): + print(f" ๐Ÿ” Error Type: {type(error).__name__}") + if hasattr(error, "status_code") and error.status_code: + print(f" ๐Ÿ“Š Status Code: {error.status_code}") + if hasattr(error, "error_code") and error.error_code: + print(f" ๐Ÿ”ข Error Code: {error.error_code}") + if hasattr(error, "response_data") and error.response_data: + print(f" ๐Ÿ“‹ Response Data: {error.response_data}") + + +if __name__ == "__main__": + main() diff --git a/examples/whatsapp_example.py b/examples/whatsapp_example.py new file mode 100644 index 0000000..a6b1e84 --- /dev/null +++ b/examples/whatsapp_example.py @@ -0,0 +1,78 @@ +import os + +from devo_global_comms_python import DevoException + + +def main(): + api_key = os.getenv("DEVO_API_KEY") + if not api_key: + print("โŒ Please set DEVO_API_KEY environment variable") + return + + print("โœ… Devo WhatsApp Client initialized successfully") + print("=" * 60) + + try: + # Example 1: Send a text message + print("๐Ÿ’ฌ WHATSAPP TEXT MESSAGE EXAMPLE") + print("-" * 30) + + print("๐Ÿ“ค Sending WhatsApp text message...") + print("โš ๏ธ This is a placeholder implementation.") + print(" Update this example when WhatsApp API is implemented.") + + # Placeholder WhatsApp send - update when implementing WhatsApp resource + print(" ```python") + print(" whatsapp_response = client.whatsapp.send_text(") + print(" to='+1234567890',") + print(" text='Hello from Devo SDK via WhatsApp!'") + print(" )") + print(" print(f'WhatsApp message sent! ID: {whatsapp_response.id}')") + print(" ```") + + # Example 2: Send media message + print("\n๐Ÿ“ท WHATSAPP MEDIA MESSAGE EXAMPLE") + print("-" * 30) + + print("๐Ÿ“ค Sending WhatsApp media message...") + print(" ```python") + print(" media_response = client.whatsapp.send_media(") + print(" to='+1234567890',") + print(" media_url='https://example.com/image.jpg',") + print(" media_type='image',") + print(" caption='Check out this image!'") + print(" )") + print(" print(f'WhatsApp media sent! ID: {media_response.id}')") + print(" ```") + + # Example 3: Send template message + print("\n๐Ÿ“‹ WHATSAPP TEMPLATE MESSAGE EXAMPLE") + print("-" * 30) + + print("๐Ÿ“ค Sending WhatsApp template message...") + print(" ```python") + print(" template_response = client.whatsapp.send_template(") + print(" to='+1234567890',") + print(" template_name='welcome_message',") + print(" template_variables={'name': 'John', 'company': 'Acme Corp'}") + print(" )") + print(" print(f'WhatsApp template sent! ID: {template_response.id}')") + print(" ```") + + except DevoException as e: + print(f"โŒ WhatsApp operation failed: {e}") + + print("\n" + "=" * 60) + print("๐Ÿ“Š WHATSAPP EXAMPLE SUMMARY") + print("-" * 30) + print("โš ๏ธ This is a placeholder example for WhatsApp functionality.") + print("๐Ÿ’ก To implement:") + print(" 1. Define WhatsApp API endpoints and specifications") + print(" 2. Create WhatsApp Pydantic models") + print(" 3. Implement WhatsAppResource class") + print(" 4. Update this example with real functionality") + print(" 5. Add support for text, media, and template messages") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index e9aa851..8aa6db1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ include = [ packages = ["src/devo_global_comms_python"] [tool.black] -line-length = 88 +line-length = 120 target-version = ['py38'] include = '\.pyi?$' extend-exclude = ''' @@ -89,7 +89,7 @@ extend-exclude = ''' [tool.isort] profile = "black" -line_length = 88 +line_length = 120 multi_line_output = 3 include_trailing_comma = true force_grid_wrap = 0 diff --git a/src/devo_global_comms_python/client.py b/src/devo_global_comms_python/client.py index e02381e..1511032 100644 --- a/src/devo_global_comms_python/client.py +++ b/src/devo_global_comms_python/client.py @@ -5,22 +5,13 @@ from urllib3.util.retry import Retry from .auth import APIKeyAuth -from .exceptions import ( - DevoAPIException, - DevoAuthenticationException, - DevoConnectionException, - DevoException, - DevoMissingAPIKeyException, - DevoTimeoutException, - create_exception_from_response, -) +from .exceptions import DevoAPIException, DevoAuthenticationException, DevoException, DevoMissingAPIKeyException from .resources.contacts import ContactsResource from .resources.email import EmailResource from .resources.messages import MessagesResource from .resources.rcs import RCSResource from .resources.sms import SMSResource from .resources.whatsapp import WhatsAppResource -from .utils import validate_email, validate_phone_number class DevoClient: @@ -33,11 +24,25 @@ class DevoClient: Example: >>> client = DevoClient(api_key="your-api-key") - >>> message = client.sms.send( - ... to="+1234567890", - ... body="Hello, World!" + >>> # Send SMS using new API + >>> response = client.sms.send_sms( + ... recipient="+1234567890", + ... message="Hello, World!", + ... sender="+0987654321" ... ) - >>> print(message.sid) + >>> print(f"Message ID: {response.id}") + >>> print(f"Status: {response.status}") + >>> + >>> # Get available senders + >>> senders = client.sms.get_senders() + >>> for sender in senders.senders: + ... print(f"Sender: {sender.phone_number}") + >>> + >>> # Get available numbers + >>> numbers = client.sms.get_available_numbers(region="US", limit=5) + >>> for number_info in numbers.numbers: + ... for feature in number_info.features: + ... print(f"Number: {feature.phone_number}") """ DEFAULT_BASE_URL = "https://global-api-development.devotel.io/api/v1" diff --git a/src/devo_global_comms_python/models/__init__.py b/src/devo_global_comms_python/models/__init__.py index 0c001e3..7e092d4 100644 --- a/src/devo_global_comms_python/models/__init__.py +++ b/src/devo_global_comms_python/models/__init__.py @@ -2,14 +2,40 @@ from .email import EmailMessage from .messages import Message from .rcs import RCSMessage -from .sms import SMSMessage +from .sms import ( + AvailableNumbersResponse, + CostInformation, + NumberFeature, + NumberInfo, + NumberPurchaseRequest, + NumberPurchaseResponse, + RegionInformation, + SenderInfo, + SendersListResponse, + SMSMessage, + SMSQuickSendRequest, + SMSQuickSendResponse, +) from .whatsapp import WhatsAppMessage __all__ = [ + # Legacy models "SMSMessage", "EmailMessage", "WhatsAppMessage", "RCSMessage", "Contact", "Message", + # New SMS API models + "SMSQuickSendRequest", + "SMSQuickSendResponse", + "NumberPurchaseRequest", + "NumberPurchaseResponse", + "SendersListResponse", + "SenderInfo", + "AvailableNumbersResponse", + "NumberInfo", + "NumberFeature", + "RegionInformation", + "CostInformation", ] diff --git a/src/devo_global_comms_python/models/sms.py b/src/devo_global_comms_python/models/sms.py index 8e5c9b8..b3efc3c 100644 --- a/src/devo_global_comms_python/models/sms.py +++ b/src/devo_global_comms_python/models/sms.py @@ -4,11 +4,161 @@ from pydantic import BaseModel, Field +# Request Models +class SMSQuickSendRequest(BaseModel): + """ + Request model for SMS quick send API. + + Used for POST /user-api/sms/quick-send + """ + + sender: str = Field(..., description="Sender phone number or ID") + recipient: str = Field(..., description="Recipient phone number in E.164 format") + message: str = Field(..., description="SMS message content") + hirvalidation: bool = Field(True, description="Enable HIR validation") + + +class NumberPurchaseRequest(BaseModel): + """ + Request model for purchasing a phone number. + + Used for POST /user-api/numbers/buy + """ + + region: str = Field(..., description="Region/country code for the number") + number: str = Field(..., description="Phone number to purchase") + number_type: str = Field(..., description="Type of number (mobile, landline, etc.)") + is_longcode: bool = Field(True, description="Whether this is a long code number") + agreement_last_sent_date: Optional[datetime] = Field(None, description="Last date agreement was sent") + agency_authorized_representative: str = Field(..., description="Name of authorized representative") + agency_representative_email: str = Field(..., description="Email of authorized representative") + is_automated_enabled: bool = Field(True, description="Whether automated messages are enabled") + + +# Response Models +class SMSQuickSendResponse(BaseModel): + """ + Response model for SMS quick send API. + + Returned from POST /user-api/sms/quick-send + """ + + id: str = Field(..., description="Unique message identifier") + user_id: str = Field(..., description="User identifier") + tenant_id: str = Field(..., description="Tenant identifier") + sender_id: str = Field(..., description="Sender identifier") + recipient: str = Field(..., description="Recipient phone number") + message: str = Field(..., description="Message content") + account_id: str = Field(..., description="Account identifier") + account_type: str = Field(..., description="Account type") + status: str = Field(..., description="Message status") + message_timeline: Dict[str, Any] = Field(default_factory=dict, description="Message timeline events") + message_id: str = Field(..., description="Message identifier") + bulksmsid: str = Field(..., description="Bulk SMS identifier") + sent_date: str = Field(..., description="Date message was sent") + direction: str = Field(..., description="Message direction") + recipientcontactid: str = Field(..., description="Recipient contact identifier") + api_route: str = Field(..., description="API route used") + apimode: str = Field(..., description="API mode") + quicksendidentifier: str = Field(..., description="Quick send identifier") + hirvalidation: bool = Field(..., description="HIR validation enabled") + + +class SenderInfo(BaseModel): + """ + Model for sender information. + """ + + id: str = Field(..., description="Sender identifier") + sender_id: str = Field(..., description="Sender ID") + gateways_id: str = Field(..., description="Gateway identifier") + phone_number: str = Field(..., description="Phone number") + number: str = Field(..., description="Number") + istest: bool = Field(..., description="Whether this is a test sender") + type: str = Field(..., description="Sender type") + + +class SendersListResponse(BaseModel): + """ + Response model for getting senders list. + + Returned from GET /user-api/me/senders + """ + + senders: List[SenderInfo] = Field(default_factory=list, description="List of available senders") + + +class RegionInformation(BaseModel): + """ + Model for region information. + """ + + region_type: str = Field(..., description="Type of region") + region_name: str = Field(..., description="Name of the region") + + +class CostInformation(BaseModel): + """ + Model for cost information. + """ + + monthly_cost: str = Field(..., description="Monthly cost") + setup_cost: str = Field(..., description="Setup cost") + currency: str = Field(..., description="Currency code") + + +class NumberFeature(BaseModel): + """ + Model for number feature information. + """ + + name: str = Field(..., description="Feature name") + reservable: bool = Field(..., description="Whether the number is reservable") + region_id: str = Field(..., description="Region identifier") + number_type: str = Field(..., description="Type of number") + quickship: bool = Field(..., description="Whether quickship is available") + region_information: RegionInformation = Field(..., description="Region details") + phone_number: str = Field(..., description="Phone number") + cost_information: CostInformation = Field(..., description="Cost details") + best_effort: bool = Field(..., description="Whether this is best effort") + number_provider_type: str = Field(..., description="Number provider type") + + +class NumberPurchaseResponse(BaseModel): + """ + Response model for number purchase API. + + Returned from POST /user-api/numbers/buy + """ + + features: List[NumberFeature] = Field(default_factory=list, description="List of number features") + + +class NumberInfo(BaseModel): + """ + Model for number information in available numbers response. + """ + + features: List[NumberFeature] = Field(default_factory=list, description="List of features for this number") + + +class AvailableNumbersResponse(BaseModel): + """ + Response model for available numbers API. + + Returned from GET /user-api/numbers + """ + + numbers: List[NumberInfo] = Field(default_factory=list, description="List of available numbers") + + +# Legacy model for backward compatibility class SMSMessage(BaseModel): """ - SMS message model. + Legacy SMS message model for backward compatibility. - Represents an SMS message sent through the Devo Global Communications API. + This model maintains compatibility with existing code while new code + should use the specific request/response models above. """ sid: str = Field(..., description="Unique identifier for the message") @@ -24,16 +174,10 @@ class SMSMessage(BaseModel): error_message: Optional[str] = Field(None, description="Error message if failed") num_segments: Optional[int] = Field(None, description="Number of message segments") media_urls: Optional[List[str]] = Field(None, description="URLs of attached media") - date_created: Optional[datetime] = Field( - None, description="Message creation timestamp" - ) + date_created: Optional[datetime] = Field(None, description="Message creation timestamp") date_sent: Optional[datetime] = Field(None, description="Message sent timestamp") - date_updated: Optional[datetime] = Field( - None, description="Message last updated timestamp" - ) - messaging_service_sid: Optional[str] = Field( - None, description="Messaging service identifier" - ) + date_updated: Optional[datetime] = Field(None, description="Message last updated timestamp") + messaging_service_sid: Optional[str] = Field(None, description="Messaging service identifier") metadata: Optional[Dict[str, Any]] = Field(None, description="Custom metadata") class Config: diff --git a/src/devo_global_comms_python/resources/sms.py b/src/devo_global_comms_python/resources/sms.py index 07984df..6f727e6 100644 --- a/src/devo_global_comms_python/resources/sms.py +++ b/src/devo_global_comms_python/resources/sms.py @@ -1,165 +1,375 @@ """ SMS resource for the Devo Global Communications API. + +Implements SMS API endpoints for sending messages and managing phone numbers. """ -from typing import TYPE_CHECKING, Any, Dict, List, Optional +import logging +from datetime import datetime +from typing import TYPE_CHECKING, List, Optional -from ..utils import validate_phone_number, validate_required_string +from ..exceptions import DevoValidationException +from ..utils import validate_email, validate_phone_number, validate_required_string from .base import BaseResource if TYPE_CHECKING: - from ..models.sms import SMSMessage + from ..models.sms import AvailableNumbersResponse, NumberPurchaseResponse, SendersListResponse, SMSQuickSendResponse + +logger = logging.getLogger(__name__) class SMSResource(BaseResource): """ - SMS resource for sending and managing SMS messages. + SMS resource for sending messages and managing phone numbers. + + This resource provides access to SMS functionality including: + - Sending SMS messages via quick-send + - Managing senders and phone numbers + - Purchasing new phone numbers + - Listing available numbers + + Examples: + Send SMS: + >>> response = client.sms.send_sms( + ... recipient="+1234567890", + ... message="Hello World!", + ... sender="+0987654321" + ... ) + + Get senders: + >>> senders = client.sms.get_senders() + >>> for sender in senders.senders: + ... print(f"Sender: {sender.phone_number}") + + Buy number: + >>> number = client.sms.buy_number( + ... region="US", + ... number="+1234567890", + ... number_type="mobile", + ... agency_authorized_representative="Jane Doe", + ... agency_representative_email="jane.doe@company.com" + ... ) - Example: - >>> message = client.sms.send( - ... to="+1234567890", - ... body="Hello, World!" + List available numbers: + >>> numbers = client.sms.get_available_numbers( + ... region="US", + ... limit=10, + ... number_type="mobile" ... ) - >>> print(message.sid) """ - def send( + def send_sms( self, - to: str, - body: str, - from_: Optional[str] = None, - media_urls: Optional[List[str]] = None, - callback_url: Optional[str] = None, - metadata: Optional[Dict[str, Any]] = None, - ) -> "SMSMessage": + recipient: str, + message: str, + sender: str, + hirvalidation: bool = True, + ) -> "SMSQuickSendResponse": """ - Send an SMS message. + Send an SMS message using the quick-send API. Args: - to: The recipient's phone number in E.164 format - body: The message body text - from_: The sender's phone number (optional, uses account default) - media_urls: List of media URLs for MMS (optional) - callback_url: Webhook URL for delivery status (optional) - metadata: Custom metadata dictionary (optional) + recipient: The recipient's phone number in E.164 format + message: The SMS message content + sender: The sender phone number or sender ID + hirvalidation: Enable HIR validation (default: True) Returns: - SMSMessage: The sent message details + SMSQuickSendResponse: The sent message details including ID and status Raises: DevoValidationException: If required fields are invalid DevoAPIException: If the API returns an error + + Example: + >>> response = client.sms.send_sms( + ... recipient="+1234567890", + ... message="Hello World!", + ... sender="+0987654321" + ... ) + >>> print(f"Message ID: {response.id}") + >>> print(f"Status: {response.status}") """ # Validate inputs - to = validate_phone_number(to) - body = validate_required_string(body, "body") + recipient = validate_phone_number(recipient) + message = validate_required_string(message, "message") + sender = validate_required_string(sender, "sender") - if from_: - from_ = validate_phone_number(from_) + logger.info(f"Sending SMS to {recipient} from {sender}") - # Prepare request data - data = { - "to": to, - "body": body, - } + # Prepare request data according to API spec + from ..models.sms import SMSQuickSendRequest - if from_: - data["from"] = from_ - if media_urls: - data["media_urls"] = media_urls - if callback_url: - data["callback_url"] = callback_url - if metadata: - data["metadata"] = metadata + request_data = SMSQuickSendRequest( + sender=sender, + recipient=recipient, + message=message, + hirvalidation=hirvalidation, + ) - # Send request - response = self.client.post("sms/messages", json=data) + # Send request to the exact API endpoint + response = self.client.post("user-api/sms/quick-send", json=request_data.dict()) - # Import here to avoid circular imports - from ..models.sms import SMSMessage + # Parse response according to API spec + from ..models.sms import SMSQuickSendResponse - return SMSMessage.parse_obj(response.json()) + result = SMSQuickSendResponse.parse_obj(response.json()) + logger.info(f"SMS sent successfully with ID: {result.id}") - def get(self, message_sid: str) -> "SMSMessage": - """ - Retrieve an SMS message by SID. + return result - Args: - message_sid: The message SID + def get_senders(self) -> "SendersListResponse": + """ + Retrieve the list of available senders for the account. Returns: - SMSMessage: The message details + SendersListResponse: List of available senders with their details + + Raises: + DevoAPIException: If the API returns an error + + Example: + >>> senders = client.sms.get_senders() + >>> for sender in senders.senders: + ... print(f"Sender: {sender.phone_number} (Type: {sender.type})") + ... print(f"Is Test: {sender.istest}") """ - message_sid = validate_required_string(message_sid, "message_sid") + logger.info("Fetching available senders") - response = self.client.get(f"sms/messages/{message_sid}") + # Send request to the exact API endpoint + response = self.client.get("user-api/me/senders") - from ..models.sms import SMSMessage + # Parse response according to API spec + from ..models.sms import SendersListResponse - return SMSMessage.parse_obj(response.json()) + result = SendersListResponse.parse_obj(response.json()) + logger.info(f"Retrieved {len(result.senders)} senders") - def list( + return result + + def buy_number( self, - to: Optional[str] = None, - from_: 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["SMSMessage"]: + region: str, + number: str, + number_type: str, + agency_authorized_representative: str, + agency_representative_email: str, + is_longcode: bool = True, + agreement_last_sent_date: Optional[datetime] = None, + is_automated_enabled: bool = True, + ) -> "NumberPurchaseResponse": """ - List SMS messages with optional filtering. + Purchase a phone number. Args: - to: Filter by recipient phone number - from_: Filter by sender phone number - 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) + region: Region/country code for the number + number: Phone number to purchase + number_type: Type of number (mobile, landline, etc.) + agency_authorized_representative: Name of authorized representative + agency_representative_email: Email of authorized representative + is_longcode: Whether this is a long code number (default: True) + agreement_last_sent_date: Last date agreement was sent (optional) + is_automated_enabled: Whether automated messages are enabled (default: True) Returns: - List[SMSMessage]: List of messages + NumberPurchaseResponse: Details of the purchased number including features + + Raises: + DevoValidationException: If required fields are invalid + DevoAPIException: If the API returns an error + + Example: + >>> number = client.sms.buy_number( + ... region="US", + ... number="+1234567890", + ... number_type="mobile", + ... agency_authorized_representative="Jane Doe", + ... agency_representative_email="jane.doe@company.com" + ... ) + >>> print(f"Purchased number with {len(number.features)} features") + """ + # Validate inputs + region = validate_required_string(region, "region") + number = validate_phone_number(number) + number_type = validate_required_string(number_type, "number_type") + agency_authorized_representative = validate_required_string( + agency_authorized_representative, "agency_authorized_representative" + ) + agency_representative_email = validate_email(agency_representative_email) + + logger.info(f"Purchasing number {number} in region {region}") + + # Prepare request data according to API spec + from ..models.sms import NumberPurchaseRequest + + request_data = NumberPurchaseRequest( + region=region, + number=number, + number_type=number_type, + is_longcode=is_longcode, + agreement_last_sent_date=agreement_last_sent_date, + agency_authorized_representative=agency_authorized_representative, + agency_representative_email=agency_representative_email, + is_automated_enabled=is_automated_enabled, + ) + + # Send request to the exact API endpoint + response = self.client.post("user-api/numbers/buy", json=request_data.dict(exclude_none=True)) + + # Parse response according to API spec + from ..models.sms import NumberPurchaseResponse + + result = NumberPurchaseResponse.parse_obj(response.json()) + logger.info(f"Number purchased successfully with {len(result.features)} features") + + return result + + def get_available_numbers( + self, + page: Optional[int] = None, + limit: Optional[int] = None, + capabilities: Optional[List[str]] = None, + type: Optional[str] = None, + prefix: Optional[str] = None, + region: str = "US", + ) -> "AvailableNumbersResponse": """ - params = { - "limit": limit, - "offset": offset, - } + Get available phone numbers for purchase. - if to: - params["to"] = validate_phone_number(to) - if from_: - params["from"] = validate_phone_number(from_) - 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 + Args: + page: The page number (optional) + limit: The page limit (optional) + capabilities: Filter by capabilities (optional) + type: Filter by type (optional) + prefix: Filter by prefix (optional) + region: Filter by region (Country ISO Code), default: "US" - response = self.client.get("sms/messages", params=params) - data = response.json() + Returns: + AvailableNumbersResponse: List of available numbers with their features - from ..models.sms import SMSMessage + Raises: + DevoValidationException: If required fields are invalid + DevoAPIException: If the API returns an error - return [SMSMessage.parse_obj(item) for item in data.get("messages", [])] + Example: + >>> numbers = client.sms.get_available_numbers( + ... region="US", + ... limit=10, + ... type="mobile" + ... ) + >>> for number_info in numbers.numbers: + ... for feature in number_info.features: + ... print(f"Number: {feature.phone_number}") + ... print(f"Cost: {feature.cost_information.monthly_cost}") + """ + logger.info(f"Fetching available numbers for region {region}") + + # Prepare query parameters + params = {"region": region} + + if page is not None: + params["page"] = page + if limit is not None: + params["limit"] = limit + if capabilities is not None: + params["capabilities"] = capabilities + if type is not None: + params["type"] = type + if prefix is not None: + params["prefix"] = prefix + + # Send request to the exact API endpoint + response = self.client.get("user-api/numbers", params=params) + + # Parse response according to API spec + from ..models.sms import AvailableNumbersResponse + + result = AvailableNumbersResponse.parse_obj(response.json()) + logger.info(f"Retrieved {len(result.numbers)} available numbers") + + return result + + # Legacy methods for backward compatibility + def send(self, to: str, body: str, from_: Optional[str] = None, **kwargs) -> "SMSQuickSendResponse": + """ + Legacy method for sending SMS (backward compatibility). + + Args: + to: The recipient's phone number in E.164 format + body: The message body text + from_: The sender's phone number (optional) + **kwargs: Additional parameters (ignored for compatibility) + + Returns: + SMSQuickSendResponse: The sent message details + + Note: + This method is deprecated. Use send_sms() instead. + """ + if not from_: + raise DevoValidationException("Sender (from_) is required for SMS sending") + + return self.send_sms( + recipient=to, + message=body, + sender=from_, + hirvalidation=kwargs.get("hirvalidation", True), + ) - def cancel(self, message_sid: str) -> "SMSMessage": + def get(self, message_id: str) -> dict: """ - Cancel a scheduled SMS message. + Legacy method for getting message details (backward compatibility). Args: - message_sid: The message SID to cancel + 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: - SMSMessage: The updated message details + 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: """ - message_sid = validate_required_string(message_sid, "message_sid") + Legacy method for canceling messages (backward compatibility). - response = self.client.delete(f"sms/messages/{message_sid}") + Args: + message_id: The message ID to cancel - from ..models.sms import SMSMessage + Returns: + dict: Cancellation result - return SMSMessage.parse_obj(response.json()) + 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/tests/test_sms.py b/tests/test_sms.py index 44323cb..af61491 100644 --- a/tests/test_sms.py +++ b/tests/test_sms.py @@ -1,4 +1,5 @@ -from unittest.mock import Mock, patch +from datetime import datetime +from unittest.mock import Mock import pytest @@ -7,7 +8,7 @@ class TestSMSResource: - """Test cases for the SMS resource.""" + """Test cases for the SMS resource with new API implementation.""" @pytest.fixture def sms_resource(self, mock_client): @@ -15,124 +16,331 @@ def sms_resource(self, mock_client): return SMSResource(mock_client) def test_send_sms_success(self, sms_resource, test_phone_number): - """Test sending an SMS successfully.""" - # Setup mock response + """Test sending an SMS successfully using the new quick-send API.""" + # Setup mock response matching the API spec mock_response = Mock() mock_response.json.return_value = { - "sid": "SMS123456789", - "to": test_phone_number, - "body": "Hello, World!", + "id": "msg_123456789", + "user_id": "user_123", + "tenant_id": "tenant_123", + "sender_id": "sender_123", + "recipient": test_phone_number, + "message": "Hello, World!", + "account_id": "account_123", + "account_type": "sms", "status": "queued", + "message_timeline": {}, + "message_id": "msg_123456789", + "bulksmsid": "bulk_123", + "sent_date": "2024-01-01T12:00:00Z", + "direction": "sent", + "recipientcontactid": "contact_123", + "api_route": "user-api/sms/quick-send", + "apimode": "quick-send", + "quicksendidentifier": "quick_123", + "hirvalidation": True, } sms_resource.client.post.return_value = mock_response - with patch("devo_global_comms_python.models.sms.SMSMessage") as mock_model: - mock_model.parse_obj.return_value = Mock(sid="SMS123456789") + # Test the new send_sms method + result = sms_resource.send_sms( + recipient=test_phone_number, + message="Hello, World!", + sender="+1987654321", + hirvalidation=True, + ) - result = sms_resource.send(to=test_phone_number, body="Hello, World!") + # Verify the response + assert result.id == "msg_123456789" + assert result.recipient == test_phone_number + assert result.message == "Hello, World!" + assert result.status == "queued" + assert result.hirvalidation is True - assert result.sid == "SMS123456789" - sms_resource.client.post.assert_called_once() + # Verify the API call + sms_resource.client.post.assert_called_once_with( + "user-api/sms/quick-send", + json={ + "sender": "+1987654321", + "recipient": test_phone_number, + "message": "Hello, World!", + "hirvalidation": True, + }, + ) - def test_send_sms_with_invalid_phone_number(self, sms_resource): - """Test sending SMS with invalid phone number.""" + def test_send_sms_with_invalid_recipient(self, sms_resource): + """Test sending SMS with invalid recipient phone number.""" with pytest.raises(DevoValidationException): - sms_resource.send(to="invalid-phone", body="Hello, World!") + sms_resource.send_sms( + recipient="invalid-phone", + message="Hello, World!", + sender="+1987654321", + ) + + def test_send_sms_with_empty_message(self, sms_resource, test_phone_number): + """Test sending SMS with empty message.""" + with pytest.raises(DevoValidationException): + sms_resource.send_sms(recipient=test_phone_number, message="", sender="+1987654321") - def test_send_sms_with_empty_body(self, sms_resource, test_phone_number): - """Test sending SMS with empty body.""" + def test_send_sms_with_empty_sender(self, sms_resource, test_phone_number): + """Test sending SMS with empty sender.""" with pytest.raises(DevoValidationException): - sms_resource.send(to=test_phone_number, body="") + sms_resource.send_sms(recipient=test_phone_number, message="Hello, World!", sender="") + + def test_get_senders_success(self, sms_resource): + """Test retrieving senders list successfully.""" + # Setup mock response matching the API spec + mock_response = Mock() + mock_response.json.return_value = { + "senders": [ + { + "id": "sender_1", + "sender_id": "sender_123", + "gateways_id": "gateway_1", + "phone_number": "+1234567890", + "number": "+1234567890", + "istest": False, + "type": "number", + }, + { + "id": "sender_2", + "sender_id": "sender_456", + "gateways_id": "gateway_2", + "phone_number": "+1987654321", + "number": "+1987654321", + "istest": True, + "type": "number", + }, + ] + } + sms_resource.client.get.return_value = mock_response - def test_send_sms_with_optional_params(self, sms_resource, test_phone_number): - """Test sending SMS with optional parameters.""" + result = sms_resource.get_senders() + + # Verify the response + assert len(result.senders) == 2 + assert result.senders[0].phone_number == "+1234567890" + assert result.senders[0].istest is False + assert result.senders[1].phone_number == "+1987654321" + assert result.senders[1].istest is True + + # Verify the API call + sms_resource.client.get.assert_called_once_with("user-api/me/senders") + + def test_buy_number_success(self, sms_resource): + """Test purchasing a phone number successfully.""" + # Setup mock response matching the API spec mock_response = Mock() - mock_response.json.return_value = {"sid": "SMS123456789"} + mock_response.json.return_value = { + "features": [ + { + "name": "SMS", + "reservable": True, + "region_id": "US", + "number_type": "mobile", + "quickship": True, + "region_information": { + "region_type": "country", + "region_name": "United States", + }, + "phone_number": "+1234567890", + "cost_information": { + "monthly_cost": "1.00", + "setup_cost": "0.00", + "currency": "USD", + }, + "best_effort": False, + "number_provider_type": "twilio", + } + ] + } sms_resource.client.post.return_value = mock_response - with patch("devo_global_comms_python.models.sms.SMSMessage") as mock_model: - mock_model.parse_obj.return_value = Mock(sid="SMS123456789") + result = sms_resource.buy_number( + region="US", + number="+1234567890", + number_type="mobile", + agency_authorized_representative="Jane Doe", + agency_representative_email="jane.doe@company.com", + is_longcode=True, + is_automated_enabled=True, + ) - sms_resource.send( - to=test_phone_number, - body="Hello, World!", - from_="+1987654321", - callback_url="https://example.com/webhook", - metadata={"campaign": "test"}, - ) + # Verify the response + assert len(result.features) == 1 + feature = result.features[0] + assert feature.phone_number == "+1234567890" + assert feature.region_information.region_name == "United States" + assert feature.cost_information.monthly_cost == "1.00" - # Verify the request was made with correct data - call_args = sms_resource.client.post.call_args - assert call_args[0][0] == "sms/messages" - assert "from" in call_args[1]["json"] - assert "callback_url" in call_args[1]["json"] - assert "metadata" in call_args[1]["json"] + # Verify the API call + call_args = sms_resource.client.post.call_args + assert call_args[0][0] == "user-api/numbers/buy" + request_data = call_args[1]["json"] + assert request_data["region"] == "US" + assert request_data["number"] == "+1234567890" + assert request_data["number_type"] == "mobile" + assert request_data["agency_authorized_representative"] == "Jane Doe" + assert request_data["agency_representative_email"] == "jane.doe@company.com" - def test_get_sms_message(self, sms_resource): - """Test retrieving an SMS message.""" - message_sid = "SMS123456789" + def test_buy_number_with_invalid_email(self, sms_resource): + """Test purchasing a number with invalid email.""" + with pytest.raises(DevoValidationException): + sms_resource.buy_number( + region="US", + number="+1234567890", + number_type="mobile", + agency_authorized_representative="Jane Doe", + agency_representative_email="invalid-email", + ) + + def test_buy_number_with_datetime(self, sms_resource): + """Test purchasing a number with agreement date.""" mock_response = Mock() - mock_response.json.return_value = {"sid": message_sid} - sms_resource.client.get.return_value = mock_response + mock_response.json.return_value = {"features": []} + sms_resource.client.post.return_value = mock_response - with patch("devo_global_comms_python.models.sms.SMSMessage") as mock_model: - mock_model.parse_obj.return_value = Mock(sid=message_sid) + agreement_date = datetime(2024, 1, 1, 12, 0, 0) - result = sms_resource.get(message_sid) + sms_resource.buy_number( + region="US", + number="+1234567890", + number_type="mobile", + agency_authorized_representative="Jane Doe", + agency_representative_email="jane.doe@company.com", + agreement_last_sent_date=agreement_date, + ) - assert result.sid == message_sid - sms_resource.client.get.assert_called_with(f"sms/messages/{message_sid}") + # Verify the API call includes the datetime + call_args = sms_resource.client.post.call_args + request_data = call_args[1]["json"] + assert "agreement_last_sent_date" in request_data - def test_list_sms_messages(self, sms_resource): - """Test listing SMS messages.""" + def test_get_available_numbers_success(self, sms_resource): + """Test getting available numbers successfully.""" + # Setup mock response matching the API spec mock_response = Mock() mock_response.json.return_value = { - "messages": [ - {"sid": "SMS1", "body": "Message 1"}, - {"sid": "SMS2", "body": "Message 2"}, + "numbers": [ + { + "features": [ + { + "name": "SMS", + "reservable": True, + "region_id": "US", + "number_type": "mobile", + "quickship": True, + "region_information": { + "region_type": "country", + "region_name": "United States", + }, + "phone_number": "+1234567890", + "cost_information": { + "monthly_cost": "1.00", + "setup_cost": "0.00", + "currency": "USD", + }, + "best_effort": False, + "number_provider_type": "twilio", + } + ] + } ] } sms_resource.client.get.return_value = mock_response - with patch("devo_global_comms_python.models.sms.SMSMessage") as mock_model: - mock_model.parse_obj.side_effect = [Mock(sid="SMS1"), Mock(sid="SMS2")] + result = sms_resource.get_available_numbers(region="US", limit=10, type="mobile", page=1) - result = sms_resource.list(limit=10, offset=0) + # Verify the response + assert len(result.numbers) == 1 + number_info = result.numbers[0] + assert len(number_info.features) == 1 + feature = number_info.features[0] + assert feature.phone_number == "+1234567890" + assert feature.number_type == "mobile" - assert len(result) == 2 - sms_resource.client.get.assert_called_with( - "sms/messages", params={"limit": 10, "offset": 0} - ) + # Verify the API call + call_args = sms_resource.client.get.call_args + assert call_args[0][0] == "user-api/numbers" + params = call_args[1]["params"] + assert params["region"] == "US" + assert params["limit"] == 10 + assert params["type"] == "mobile" + assert params["page"] == 1 - def test_list_sms_messages_with_filters(self, sms_resource, test_phone_number): - """Test listing SMS messages with filters.""" + def test_get_available_numbers_default_region(self, sms_resource): + """Test getting available numbers with default region.""" mock_response = Mock() - mock_response.json.return_value = {"messages": []} + mock_response.json.return_value = {"numbers": []} sms_resource.client.get.return_value = mock_response - with patch("devo_global_comms_python.models.sms.SMSMessage"): - sms_resource.list( - to=test_phone_number, status="delivered", date_sent_after="2024-01-01" - ) + sms_resource.get_available_numbers() - call_args = sms_resource.client.get.call_args - params = call_args[1]["params"] - assert params["to"] == test_phone_number - assert params["status"] == "delivered" - assert params["date_sent_after"] == "2024-01-01" + # Verify default region is used + call_args = sms_resource.client.get.call_args + params = call_args[1]["params"] + assert params["region"] == "US" - def test_cancel_sms_message(self, sms_resource): - """Test canceling an SMS message.""" - message_sid = "SMS123456789" + def test_get_available_numbers_with_capabilities(self, sms_resource): + """Test getting available numbers with capabilities filter.""" mock_response = Mock() - mock_response.json.return_value = {"sid": message_sid, "status": "canceled"} - sms_resource.client.delete.return_value = mock_response + mock_response.json.return_value = {"numbers": []} + sms_resource.client.get.return_value = mock_response + + capabilities = ["SMS", "MMS"] + sms_resource.get_available_numbers(capabilities=capabilities) + + # Verify capabilities are passed + call_args = sms_resource.client.get.call_args + params = call_args[1]["params"] + assert params["capabilities"] == capabilities + + # Legacy method tests for backward compatibility + def test_legacy_send_method(self, sms_resource, test_phone_number): + """Test legacy send method for backward compatibility.""" + mock_response = Mock() + mock_response.json.return_value = { + "id": "msg_123456789", + "user_id": "user_123", + "tenant_id": "tenant_123", + "sender_id": "sender_123", + "recipient": test_phone_number, + "message": "Hello, World!", + "account_id": "account_123", + "account_type": "sms", + "status": "queued", + "message_timeline": {}, + "message_id": "msg_123456789", + "bulksmsid": "bulk_123", + "sent_date": "2024-01-01T12:00:00Z", + "direction": "sent", + "recipientcontactid": "contact_123", + "api_route": "user-api/sms/quick-send", + "apimode": "quick-send", + "quicksendidentifier": "quick_123", + "hirvalidation": True, + } + sms_resource.client.post.return_value = mock_response + + # Test legacy method + result = sms_resource.send(to=test_phone_number, body="Hello, World!", from_="+1987654321") + + # Should return the new response type + assert result.id == "msg_123456789" + assert result.recipient == test_phone_number + + def test_legacy_send_without_sender_fails(self, sms_resource, test_phone_number): + """Test legacy send method fails without sender.""" + with pytest.raises(DevoValidationException, match="Sender .* is required"): + sms_resource.send(to=test_phone_number, body="Hello, World!") - with patch("devo_global_comms_python.models.sms.SMSMessage") as mock_model: - mock_model.parse_obj.return_value = Mock(sid=message_sid, status="canceled") + 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") - result = sms_resource.cancel(message_sid) + with pytest.raises(NotImplementedError): + sms_resource.list() - assert result.sid == message_sid - assert result.status == "canceled" - sms_resource.client.delete.assert_called_with(f"sms/messages/{message_sid}") + with pytest.raises(NotImplementedError): + sms_resource.cancel("msg_123")