From 415217615b8ee8bbcb9a97a92b11d77fd8a581e7 Mon Sep 17 00:00:00 2001 From: rocribera Date: Mon, 25 May 2026 14:55:09 +0200 Subject: [PATCH 1/5] feat: add async support --- README.md | 263 ++++++++++++--- mailersend/__init__.py | 4 +- mailersend/async_client.py | 302 +++++++++++++++++ mailersend/exceptions.py | 5 +- mailersend/resources/__init__.py | 77 +++-- mailersend/resources/activity.py | 39 ++- mailersend/resources/analytics.py | 74 ++++- mailersend/resources/base.py | 17 +- mailersend/resources/dmarc_monitoring.py | 176 +++++++++- mailersend/resources/domains.py | 144 +++++++- mailersend/resources/email.py | 57 +++- mailersend/resources/email_verification.py | 137 +++++++- mailersend/resources/identities.py | 142 +++++++- mailersend/resources/inbound.py | 82 ++++- mailersend/resources/messages.py | 37 ++- mailersend/resources/other.py | 16 +- mailersend/resources/recipients.py | 310 ++++++++++++++++- mailersend/resources/schedules.py | 52 ++- mailersend/resources/sms_activity.py | 37 ++- mailersend/resources/sms_inbounds.py | 79 ++++- mailersend/resources/sms_messages.py | 37 ++- mailersend/resources/sms_numbers.py | 69 +++- mailersend/resources/sms_recipients.py | 58 +++- mailersend/resources/sms_sending.py | 20 +- mailersend/resources/sms_webhooks.py | 82 ++++- mailersend/resources/smtp_users.py | 83 ++++- mailersend/resources/templates.py | 56 +++- mailersend/resources/tokens.py | 91 ++++- mailersend/resources/users.py | 132 +++++++- mailersend/resources/webhooks.py | 77 ++++- poetry.lock | 121 ++++++- pyproject.toml | 6 + tests/unit/test_async_activity_resource.py | 67 ++++ tests/unit/test_async_analytics_resource.py | 83 +++++ tests/unit/test_async_client.py | 311 ++++++++++++++++++ .../test_async_dmarc_monitoring_resource.py | 153 +++++++++ tests/unit/test_async_domains_resource.py | 148 +++++++++ tests/unit/test_async_email_resource.py | 89 +++++ .../test_async_email_verification_resource.py | 155 +++++++++ tests/unit/test_async_identities_resource.py | 141 ++++++++ tests/unit/test_async_inbound_resource.py | 124 +++++++ tests/unit/test_async_messages_resource.py | 58 ++++ tests/unit/test_async_other_resource.py | 32 ++ tests/unit/test_async_recipients_resource.py | 282 ++++++++++++++++ tests/unit/test_async_schedules_resource.py | 64 ++++ .../unit/test_async_sms_activity_resource.py | 46 +++ .../unit/test_async_sms_inbounds_resource.py | 100 ++++++ .../unit/test_async_sms_messages_resource.py | 60 ++++ tests/unit/test_async_sms_numbers_resource.py | 74 +++++ .../test_async_sms_recipients_resource.py | 77 +++++ tests/unit/test_async_sms_sending_resource.py | 43 +++ .../unit/test_async_sms_webhooks_resource.py | 109 ++++++ tests/unit/test_async_smtp_users_resource.py | 94 ++++++ tests/unit/test_async_templates_resource.py | 72 ++++ tests/unit/test_async_tokens_resource.py | 105 ++++++ tests/unit/test_async_users_resource.py | 133 ++++++++ tests/unit/test_async_webhooks_resource.py | 103 ++++++ tests/unit/test_webhooks_builder.py | 6 +- 58 files changed, 5502 insertions(+), 109 deletions(-) create mode 100644 mailersend/async_client.py create mode 100644 tests/unit/test_async_activity_resource.py create mode 100644 tests/unit/test_async_analytics_resource.py create mode 100644 tests/unit/test_async_client.py create mode 100644 tests/unit/test_async_dmarc_monitoring_resource.py create mode 100644 tests/unit/test_async_domains_resource.py create mode 100644 tests/unit/test_async_email_resource.py create mode 100644 tests/unit/test_async_email_verification_resource.py create mode 100644 tests/unit/test_async_identities_resource.py create mode 100644 tests/unit/test_async_inbound_resource.py create mode 100644 tests/unit/test_async_messages_resource.py create mode 100644 tests/unit/test_async_other_resource.py create mode 100644 tests/unit/test_async_recipients_resource.py create mode 100644 tests/unit/test_async_schedules_resource.py create mode 100644 tests/unit/test_async_sms_activity_resource.py create mode 100644 tests/unit/test_async_sms_inbounds_resource.py create mode 100644 tests/unit/test_async_sms_messages_resource.py create mode 100644 tests/unit/test_async_sms_numbers_resource.py create mode 100644 tests/unit/test_async_sms_recipients_resource.py create mode 100644 tests/unit/test_async_sms_sending_resource.py create mode 100644 tests/unit/test_async_sms_webhooks_resource.py create mode 100644 tests/unit/test_async_smtp_users_resource.py create mode 100644 tests/unit/test_async_templates_resource.py create mode 100644 tests/unit/test_async_tokens_resource.py create mode 100644 tests/unit/test_async_users_resource.py create mode 100644 tests/unit/test_async_webhooks_resource.py diff --git a/README.md b/README.md index 1fa10cd..0d31354 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ MailerSend Python SDK [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) # Table of Contents + - [Table of Contents](#table-of-contents) - [Installation](#installation) - [Requirements](#requirements) @@ -194,6 +195,11 @@ MailerSend Python SDK - [Remove IP from favorites](#remove-ip-from-favorites) - [Other Endpoints](#other-endpoints) - [Get API Quota](#get-api-quota) + - [Async Support](#async-support) + - [Basic Async Usage](#basic-async-usage) + - [Concurrent Requests](#concurrent-requests) + - [Async Error Handling](#async-error-handling) + - [Async Debug Logging](#async-debug-logging) - [Error Handling](#error-handling) - [Testing](#testing) - [Running Unit Tests](#running-unit-tests) @@ -212,7 +218,7 @@ pip install mailersend ## Requirements -- Python 3.7+ +- Python 3.10+ - An API Key from [mailersend.com](https://www.mailersend.com) ## Authentication @@ -278,7 +284,7 @@ ms = MailerSendClient(api_key="your-api-key") # SDK Architecture -The MailerSend Python SDK v2 introduces a modern, clean architecture that follows industry best practices: +The MailerSend Python SDK v2 introduces a modern, clean architecture that follows industry best practices. Both a synchronous client (`MailerSendClient`) and an async client (`AsyncMailerSendClient`) are available — they share the same resources, builders, and models. ## Builder Pattern @@ -309,7 +315,7 @@ Each API endpoint group has its own resource class that provides clean method in ```python # Access different API resources ms.sms_recipients # SMS Recipients operations -ms.sms_webhooks # SMS Webhooks operations +ms.sms_webhooks # SMS Webhooks operations ms.sms_inbounds # SMS Inbound Routing operations ms.email # Email operations ms.domains # Domain operations @@ -337,6 +343,7 @@ The MailerSend SDK provides flexible ways to access and work with API response d ## Multiple Access Patterns ### Dict-like Access + Access response data using dictionary-style syntax: ```python @@ -362,6 +369,7 @@ if "error" in response: ``` ### Attribute Access + Access data using dot notation for cleaner code: ```python @@ -376,6 +384,7 @@ if hasattr(response, 'sms') and response.sms: ``` ### Safe Access with Defaults + Use the `get()` method for safe access with fallback values: ```python @@ -390,6 +399,7 @@ current_page = meta_info.get("page", 1) ``` ### Handling Method Name Conflicts + When response data contains fields that conflict with built-in methods, use the `data_` prefix: ```python @@ -413,6 +423,7 @@ value_list = response.data_values ## Data Format Conversion ### Convert to Dictionary + Get the complete response as a dictionary: ```python @@ -437,6 +448,7 @@ headers_only = response_dict["headers"] ``` ### Convert to JSON + Get JSON string representation with various formatting options: ```python @@ -455,6 +467,7 @@ json_string = json.dumps(response) ``` ### Extract Raw Data + Access just the API response data without metadata: ```python @@ -474,6 +487,7 @@ else: ## Headers and Metadata ### Access Response Headers + Headers can be accessed in multiple ways with automatic case handling: ```python @@ -494,6 +508,7 @@ retry_after = response.headers.get("retry-after", "0") ``` ### Response Metadata + Access useful metadata about the API response: ```python @@ -518,6 +533,7 @@ if "meta" in response.data: ## Error Handling with Responses ### Check Response Status + Always check if the response was successful: ```python @@ -528,33 +544,34 @@ ms = MailerSendClient() try: email = EmailBuilder().from_email("sender@domain.com").build() response = ms.emails.send(email) - + if response.success: email_id = response.id remaining_quota = response.rate_limit_remaining else: status_code = response.status_code error_details = response.data - + # Handle rate limiting if response.status_code == 429 and response.retry_after: retry_seconds = response.retry_after - + except Exception as e: # Handle exception ``` ### Access Error Information + When requests fail, error details are available in the response: ```python if not response.success: error_data = response.data - + # API error response structure error_message = error_data.get("message", "Unknown error") error_code = error_data.get("code") - + # Validation errors (422 responses) if "errors" in error_data: for field, messages in error_data["errors"].items(): @@ -575,7 +592,7 @@ users_response = ms.users.list_users(request) if users_response.success: users = users_response.data["data"] # Array of users total_count = users_response.data["meta"]["total"] - + for user in users: user_name = user['name'] user_email = user['email'] @@ -1543,7 +1560,7 @@ from mailersend import MailerSendClient, RecipientsBuilder ms = MailerSendClient() -# Delete specific entries by IDs +# Delete specific entries by IDs request = (RecipientsBuilder() .domain_id("domain-id") .ids(["recipient-id"]) @@ -2050,7 +2067,7 @@ request = (SmsSendingBuilder() "data": {"name": "John", "order_id": "12345"} }, { - "phone_number": "+1234567891", + "phone_number": "+1234567891", "data": {"name": "Jane", "order_id": "12346"} } ]) @@ -2840,6 +2857,164 @@ ms = MailerSendClient() response = ms.api_quota.get_quota() ``` + + +## Async Support + +The SDK provides a fully async-compatible client, `AsyncMailerSendClient`, built on `httpx.AsyncClient`. It exposes the same resources and methods as the synchronous `MailerSendClient` — prefixed with `async`/`await` — so you can use it anywhere `asyncio` is available. + +### Basic Async Usage + +Use `AsyncMailerSendClient` as an async context manager (recommended) to ensure the underlying HTTP connection is properly closed: + +```python +import asyncio +from mailersend import AsyncMailerSendClient, EmailBuilder + +async def main(): + async with AsyncMailerSendClient() as client: + email = (EmailBuilder() + .from_email("sender@domain.com", "Your Name") + .to_many([{"email": "recipient@domain.com", "name": "Recipient"}]) + .subject("Hello from MailerSend!") + .html("

Hello World!

") + .text("Hello World!") + .build()) + + response = await client.emails.send(email) + print(response.status_code) + +asyncio.run(main()) +``` + +If you prefer to manage the lifecycle manually, call `await client.close()` when finished: + +```python +from mailersend import AsyncMailerSendClient + +client = AsyncMailerSendClient(api_key="your-api-key") + +try: + response = await client.api_quota.get_quota() +finally: + await client.close() +``` + +All resources available on `MailerSendClient` are also available on `AsyncMailerSendClient`: + +```python +async with AsyncMailerSendClient() as client: + client.emails # Email operations + client.activities # Activity operations + client.analytics # Analytics operations + client.domains # Domain operations + client.identities # Sender identity operations + client.inbound # Inbound route operations + client.templates # Template operations + client.tokens # Token operations + client.webhooks # Webhook operations + client.email_verification # Email verification operations + client.users # User operations + client.messages # Message operations + client.recipients # Recipient & suppression operations + client.schedules # Scheduled message operations + client.smtp_users # SMTP user operations + client.sms_sending # SMS sending operations + client.sms_numbers # SMS phone number operations + client.sms_activity # SMS activity operations + client.sms_inbounds # SMS inbound routing operations + client.sms_recipients # SMS recipient operations + client.sms_webhooks # SMS webhook operations + client.sms_messages # SMS message operations + client.api_quota # API quota operations + client.dmarc_monitoring # DMARC monitoring operations +``` + +### Concurrent Requests + +The main benefit of `AsyncMailerSendClient` is the ability to run multiple API calls concurrently with `asyncio.gather`: + +```python +import asyncio +from mailersend import AsyncMailerSendClient, DomainsBuilder, TemplatesBuilder + +async def main(): + async with AsyncMailerSendClient() as client: + domains_request = DomainsBuilder().build_list_request() + templates_request = TemplatesBuilder().build_list_request() + + # Both requests run concurrently + domains_response, templates_response = await asyncio.gather( + client.domains.list_domains(domains_request), + client.templates.list_templates(templates_request), + ) + + print(f"Domains: {domains_response.data}") + print(f"Templates: {templates_response.data}") + +asyncio.run(main()) +``` + +### Async Error Handling + +`AsyncMailerSendClient` raises the same exception types as the synchronous client: + +```python +import asyncio +from mailersend import AsyncMailerSendClient +from mailersend.exceptions import ( + AuthenticationError, + RateLimitExceeded, + ResourceNotFoundError, + BadRequestError, + ServerError, + MailerSendError, +) + +async def main(): + async with AsyncMailerSendClient() as client: + try: + response = await client.api_quota.get_quota() + except AuthenticationError: + print("Invalid API key") + except RateLimitExceeded as e: + print(f"Rate limit hit: {e}") + except ResourceNotFoundError: + print("Resource not found") + except BadRequestError as e: + print(f"Bad request: {e}") + except ServerError as e: + print(f"Server error: {e}") + except MailerSendError as e: + print(f"Unexpected error: {e}") + +asyncio.run(main()) +``` + +The client automatically retries transient errors (429, 500, 502, 503, 504) with exponential backoff. For 429 responses the `Retry-After` header is respected if present. + +### Async Debug Logging + +Debug logging works the same way as the synchronous client: + +```python +import asyncio +from mailersend import AsyncMailerSendClient + +async def main(): + # Enable debug at construction time + async with AsyncMailerSendClient(debug=True) as client: + response = await client.api_quota.get_quota() + + # Or toggle at runtime + async with AsyncMailerSendClient() as client: + client.enable_debug() + response = await client.api_quota.get_quota() + client.disable_debug() + +asyncio.run(main()) +``` + # Error Handling @@ -2860,14 +3035,14 @@ try: .subject("Test") .html("

Test

") .build()) - + response = ms.emails.send(email) - + except MailerSendError as e: print(f"MailerSend API Error: {e}") print(f"Status Code: {e.status_code}") print(f"Error Details: {e.details}") - + except Exception as e: print(f"Unexpected error: {e}") ``` @@ -2919,36 +3094,36 @@ def test_list_sms_recipients(): # Available endpoints -| Feature group | Endpoint | Available | -|-----------------------|-----------------------------------------|-----------| -| Activity | `GET activity` | ✅ | -| Analytics | `GET analytics` | ✅ | -| Domains | `{GET, POST, PUT, DELETE} domains` | ✅ | -| Email | `POST send` | ✅ | -| Email Verification | `{GET, POST, PUT} email-verification` | ✅ | -| Bulk Email | `POST bulk-email` | ✅ | -| Inbound Routes | `{GET, POST, PUT, DELETE} inbound` | ✅ | -| Messages | `GET messages` | ✅ | -| Scheduled Messages | `{GET, DELETE} scheduled-messages` | ✅ | -| Recipients | `{GET, POST, DELETE} recipients` | ✅ | -| Templates | `{GET, DELETE} templates` | ✅ | -| Tokens | `{POST, PUT, DELETE} tokens` | ✅ | -| SMTP Users | `{GET, POST, PUT, DELETE} smtp-users` | ✅ | -| Users | `{GET, POST, PUT, DELETE} users` | ✅ | -| User Invites | `{GET, POST, DELETE} invites` | ✅ | -| Webhooks | `{GET, POST, PUT, DELETE} webhooks` | ✅ | -| SMS Sending | `POST sms` | ✅ | -| SMS Activity | `GET sms-activity` | ✅ | -| SMS Phone Numbers | `{GET, PUT, DELETE} sms-numbers` | ✅ | -| SMS Recipients | `{GET, PUT} sms-recipients` | ✅ | -| SMS Messages | `GET sms-messages` | ✅ | -| SMS Webhooks | `{GET, POST, PUT, DELETE} sms-webhooks` | ✅ | -| SMS Inbound Routing | `{GET, POST, PUT, DELETE} sms-inbounds` | ✅ | -| Sender Identities | `{GET, POST, PUT, DELETE} identities` | ✅ | -| API Quota | `GET api-quota` | ✅ | -| DMARC Monitoring | `{GET, POST, PUT, DELETE} dmarc-monitoring` | ✅ | - -*All endpoints are available and fully tested. Refer to [official API docs](https://developers.mailersend.com/) for the most up-to-date API specifications.* +| Feature group | Endpoint | Available | +| ------------------- | ------------------------------------------- | --------- | +| Activity | `GET activity` | ✅ | +| Analytics | `GET analytics` | ✅ | +| Domains | `{GET, POST, PUT, DELETE} domains` | ✅ | +| Email | `POST send` | ✅ | +| Email Verification | `{GET, POST, PUT} email-verification` | ✅ | +| Bulk Email | `POST bulk-email` | ✅ | +| Inbound Routes | `{GET, POST, PUT, DELETE} inbound` | ✅ | +| Messages | `GET messages` | ✅ | +| Scheduled Messages | `{GET, DELETE} scheduled-messages` | ✅ | +| Recipients | `{GET, POST, DELETE} recipients` | ✅ | +| Templates | `{GET, DELETE} templates` | ✅ | +| Tokens | `{POST, PUT, DELETE} tokens` | ✅ | +| SMTP Users | `{GET, POST, PUT, DELETE} smtp-users` | ✅ | +| Users | `{GET, POST, PUT, DELETE} users` | ✅ | +| User Invites | `{GET, POST, DELETE} invites` | ✅ | +| Webhooks | `{GET, POST, PUT, DELETE} webhooks` | ✅ | +| SMS Sending | `POST sms` | ✅ | +| SMS Activity | `GET sms-activity` | ✅ | +| SMS Phone Numbers | `{GET, PUT, DELETE} sms-numbers` | ✅ | +| SMS Recipients | `{GET, PUT} sms-recipients` | ✅ | +| SMS Messages | `GET sms-messages` | ✅ | +| SMS Webhooks | `{GET, POST, PUT, DELETE} sms-webhooks` | ✅ | +| SMS Inbound Routing | `{GET, POST, PUT, DELETE} sms-inbounds` | ✅ | +| Sender Identities | `{GET, POST, PUT, DELETE} identities` | ✅ | +| API Quota | `GET api-quota` | ✅ | +| DMARC Monitoring | `{GET, POST, PUT, DELETE} dmarc-monitoring` | ✅ | + +_All endpoints are available and fully tested. Refer to [official API docs](https://developers.mailersend.com/) for the most up-to-date API specifications._ diff --git a/mailersend/__init__.py b/mailersend/__init__.py index f8fe436..1ec9e83 100644 --- a/mailersend/__init__.py +++ b/mailersend/__init__.py @@ -5,6 +5,7 @@ """ from .client import MailerSendClient +from .async_client import AsyncMailerSendClient # Import all builders for better UX - users can import everything from main module from .builders.email import EmailBuilder @@ -65,8 +66,9 @@ __version__ = "2.0.0" __all__ = [ - # Core client + # Core clients "MailerSendClient", + "AsyncMailerSendClient", # Builders - All available from main module for better UX "EmailBuilder", "ActivityBuilder", diff --git a/mailersend/async_client.py b/mailersend/async_client.py new file mode 100644 index 0000000..4a53005 --- /dev/null +++ b/mailersend/async_client.py @@ -0,0 +1,302 @@ +import asyncio +import logging +import os +from typing import Any, Dict, Optional +from urllib.parse import urljoin + +import httpx + +from .constants import DEFAULT_BASE_URL, DEFAULT_TIMEOUT, USER_AGENT +from .exceptions import ( + AuthenticationError, + BadRequestError, + MailerSendError, + RateLimitExceeded, + ResourceNotFoundError, + ServerError, +) +from .resources.activity import AsyncActivity +from .resources.analytics import AsyncAnalytics +from .resources.dmarc_monitoring import AsyncDmarcMonitoring +from .resources.domains import AsyncDomains +from .resources.email import AsyncEmail +from .resources.email_verification import AsyncEmailVerification +from .resources.identities import AsyncIdentitiesResource +from .resources.inbound import AsyncInboundResource +from .resources.messages import AsyncMessages +from .resources.other import AsyncOther +from .resources.recipients import AsyncRecipients +from .resources.schedules import AsyncSchedules +from .resources.sms_activity import AsyncSmsActivity +from .resources.sms_inbounds import AsyncSmsInbounds +from .resources.sms_messages import AsyncSmsMessages +from .resources.sms_numbers import AsyncSmsNumbers +from .resources.sms_recipients import AsyncSmsRecipients +from .resources.sms_sending import AsyncSmsSending +from .resources.sms_webhooks import AsyncSmsWebhooks +from .resources.smtp_users import AsyncSmtpUsers +from .resources.templates import AsyncTemplates +from .resources.tokens import AsyncTokens +from .resources.users import AsyncUsers +from .resources.webhooks import AsyncWebhooks +from .logging import get_logger, RequestLogger + +_RETRY_STATUSES = frozenset([429, 500, 502, 503, 504]) + + +class AsyncMailerSendClient: + """ + Async client for the MailerSend API. + + Uses httpx.AsyncClient under the hood. Supports use as an async context + manager (recommended) or manual lifecycle management via close(). + + Examples: + >>> # Using environment variable (recommended) + >>> async with AsyncMailerSendClient() as client: + ... response = await client.emails.send(email_request) + + >>> # Using explicit API key + >>> client = AsyncMailerSendClient(api_key="your_api_key") + + >>> # Enable debug logging for detailed request/response info + >>> client = AsyncMailerSendClient(debug=True) + """ + + def __init__( + self, + api_key: Optional[str] = None, + base_url: str = DEFAULT_BASE_URL, + timeout: int = DEFAULT_TIMEOUT, + max_retries: int = 3, + debug: bool = False, + logger: Optional[logging.Logger] = None, + ) -> None: + """ + Initialize the async MailerSend client. + + Args: + api_key: Your MailerSend API key. If not provided, will try to read + from MAILERSEND_API_KEY environment variable + base_url: Base URL for API requests + timeout: Request timeout in seconds + max_retries: Maximum number of retries for failed requests + debug: Enable detailed debug logging + logger: Custom logger instance + + Raises: + ValueError: If no API key is provided and MAILERSEND_API_KEY + environment variable is not set + """ + resolved_api_key = api_key or os.getenv("MAILERSEND_API_KEY") + + if not resolved_api_key: + raise ValueError( + "API key is required. Either pass it as 'api_key' parameter or " + "set the 'MAILERSEND_API_KEY' environment variable." + ) + + self.api_key = resolved_api_key + self.base_url = base_url + self.timeout = timeout + self.max_retries = max_retries + self.debug = debug + self.logger = logger or get_logger(debug=debug) + self.request_logger = RequestLogger(self.logger) + + self._client = httpx.AsyncClient( + headers={ + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + "Accept": "application/json", + "User-Agent": USER_AGENT, + }, + timeout=self.timeout, + ) + + self.emails = AsyncEmail(self) + self.activities = AsyncActivity(self) + self.analytics = AsyncAnalytics(self) + self.domains = AsyncDomains(self) + self.identities = AsyncIdentitiesResource(self) + self.inbound = AsyncInboundResource(self) + self.templates = AsyncTemplates(self) + self.tokens = AsyncTokens(self) + self.webhooks = AsyncWebhooks(self) + self.email_verification = AsyncEmailVerification(self) + self.users = AsyncUsers(self) + self.messages = AsyncMessages(self) + self.recipients = AsyncRecipients(self) + self.schedules = AsyncSchedules(self) + self.sms_messages = AsyncSmsMessages(self) + self.smtp_users = AsyncSmtpUsers(self) + self.sms_sending = AsyncSmsSending(self) + self.sms_numbers = AsyncSmsNumbers(self) + self.sms_activity = AsyncSmsActivity(self) + self.sms_inbounds = AsyncSmsInbounds(self) + self.sms_recipients = AsyncSmsRecipients(self) + self.sms_webhooks = AsyncSmsWebhooks(self) + self.api_quota = AsyncOther(self) + self.dmarc_monitoring = AsyncDmarcMonitoring(self) + + self.logger.info("AsyncMailerSendClient initialized successfully") + + def enable_debug(self) -> None: + """Enable debug logging for this client instance.""" + self.debug = True + self.logger.setLevel(logging.DEBUG) + self.logger.info("Debug mode enabled") + + def disable_debug(self) -> None: + """Disable debug logging for this client instance.""" + self.debug = False + self.logger.setLevel(logging.WARNING) + self.logger.info("Debug mode disabled") + + def get_debug_info(self) -> Dict[str, Any]: + """Get current debug and configuration information.""" + return { + "debug_enabled": self.debug, + "base_url": self.base_url, + "timeout": self.timeout, + "max_retries": self.max_retries, + "user_agent": USER_AGENT, + "logger_level": self.logger.level, + } + + async def request( + self, + method: str, + path: str, + params: Optional[Dict[str, Any]] = None, + body: Optional[Any] = None, + ) -> httpx.Response: + """ + Make an async HTTP request to the MailerSend API. + + Args: + method: HTTP method (GET, POST, PUT, DELETE) + path: API endpoint path + params: Query parameters + body: Request body data + + Returns: + Response object + + Raises: + AuthenticationError: If authentication fails + ResourceNotFoundError: If the requested resource is not found + RateLimitExceeded: If API rate limits are exceeded + BadRequestError: If the request was malformed + ServerError: If a server error occurs + MailerSendError: For other API errors + """ + url = urljoin(self.base_url, path) + request_id = self.request_logger.start_request(method, url, params, body) + + last_exception: Optional[Exception] = None + + for attempt in range(self.max_retries + 1): + try: + response = await self._client.request( + method=method, + url=url, + params=params, + json=body, + ) + + self.request_logger.log_response(response) + + if 200 <= response.status_code < 300: + return response + + # Retry on transient errors (except on the last attempt) + if ( + response.status_code in _RETRY_STATUSES + and attempt < self.max_retries + ): + if response.status_code == 429: + retry_after = response.headers.get("retry-after") + delay = ( + float(retry_after) if retry_after else 0.3 * (2**attempt) + ) + else: + delay = 0.3 * (2**attempt) + self.request_logger.log_retry(attempt + 1, delay) + await asyncio.sleep(delay) + continue + + error_message = self._get_error_message(response) + self.logger.error( + f"API error {response.status_code}: {error_message}", + extra={"request_id": request_id}, + ) + + if response.status_code == 401: + raise AuthenticationError(error_message, response) + elif response.status_code == 404: + raise ResourceNotFoundError(error_message, response) + elif response.status_code == 429: + retry_after = response.headers.get("retry-after") + remaining = response.headers.get("x-apiquota-remaining") + self.logger.warning( + f"Rate limit exceeded. Retry after: {retry_after}s, Remaining: {remaining}", + extra={"request_id": request_id}, + ) + raise RateLimitExceeded(error_message, response) + elif 400 <= response.status_code < 500: + raise BadRequestError(error_message, response) + elif 500 <= response.status_code < 600: + raise ServerError(error_message, response) + else: + raise MailerSendError(error_message, response) + + except ( + AuthenticationError, + ResourceNotFoundError, + RateLimitExceeded, + BadRequestError, + ServerError, + MailerSendError, + ): + raise + except httpx.RequestError as e: + last_exception = e + if attempt < self.max_retries: + delay = 0.3 * (2**attempt) + self.request_logger.log_retry(attempt + 1, delay) + await asyncio.sleep(delay) + continue + self.request_logger.log_error(e) + raise MailerSendError(f"Request failed: {str(e)}") from e + + # Should not be reached, but satisfies type checker + if last_exception: + raise MailerSendError(f"Request failed: {str(last_exception)}") + raise MailerSendError("Request failed after retries") + + def _get_error_message(self, response: httpx.Response) -> str: + try: + error_data = response.json() + if isinstance(error_data, dict): + message = error_data.get("message", "Unknown error") + errors = error_data.get("errors", {}) + if errors: + error_details = "; ".join( + f"{key}: {', '.join(msgs)}" for key, msgs in errors.items() + ) + return f"{message}: {error_details}" + return message + except Exception: + pass + return f"Error {response.status_code}: {response.text}" + + async def close(self) -> None: + """Close the underlying httpx client and release resources.""" + await self._client.aclose() + + async def __aenter__(self) -> "AsyncMailerSendClient": + return self + + async def __aexit__(self, *_: Any) -> None: + await self.close() diff --git a/mailersend/exceptions.py b/mailersend/exceptions.py index 9611bee..cd13753 100644 --- a/mailersend/exceptions.py +++ b/mailersend/exceptions.py @@ -1,11 +1,10 @@ -from typing import Optional -import requests +from typing import Any, Optional class MailerSendError(Exception): """Base exception for all MailerSend API errors.""" - def __init__(self, message: str, response: Optional[requests.Response] = None): + def __init__(self, message: str, response: Optional[Any] = None): self.message = message self.response = response super().__init__(self.message) diff --git a/mailersend/resources/__init__.py b/mailersend/resources/__init__.py index 44e93ee..d51d779 100644 --- a/mailersend/resources/__init__.py +++ b/mailersend/resources/__init__.py @@ -2,32 +2,34 @@ API resource classes for interacting with specific MailerSend API endpoints. """ -from .base import BaseResource -from .email import Email -from .activity import Activity -from .analytics import Analytics -from .domains import Domains -from .identities import IdentitiesResource -from .inbound import InboundResource -from .messages import Messages -from .schedules import Schedules -from .recipients import Recipients -from .templates import Templates -from .tokens import Tokens -from .webhooks import Webhooks -from .email_verification import EmailVerification -from .users import Users -from .sms_messages import SmsMessages -from .sms_numbers import SmsNumbers -from .sms_activity import SmsActivity -from .sms_sending import SmsSending -from .sms_recipients import SmsRecipients -from .sms_webhooks import SmsWebhooks -from .sms_inbounds import SmsInbounds -from .other import Other -from .dmarc_monitoring import DmarcMonitoring +from .base import AsyncBaseResource, BaseResource +from .email import AsyncEmail, Email +from .activity import AsyncActivity, Activity +from .analytics import AsyncAnalytics, Analytics +from .domains import AsyncDomains, Domains +from .identities import AsyncIdentitiesResource, IdentitiesResource +from .inbound import AsyncInboundResource, InboundResource +from .messages import AsyncMessages, Messages +from .schedules import AsyncSchedules, Schedules +from .recipients import AsyncRecipients, Recipients +from .templates import AsyncTemplates, Templates +from .tokens import AsyncTokens, Tokens +from .webhooks import AsyncWebhooks, Webhooks +from .email_verification import AsyncEmailVerification, EmailVerification +from .users import AsyncUsers, Users +from .sms_messages import AsyncSmsMessages, SmsMessages +from .sms_numbers import AsyncSmsNumbers, SmsNumbers +from .sms_activity import AsyncSmsActivity, SmsActivity +from .sms_sending import AsyncSmsSending, SmsSending +from .sms_recipients import AsyncSmsRecipients, SmsRecipients +from .sms_webhooks import AsyncSmsWebhooks, SmsWebhooks +from .sms_inbounds import AsyncSmsInbounds, SmsInbounds +from .other import AsyncOther, Other +from .dmarc_monitoring import AsyncDmarcMonitoring, DmarcMonitoring +from .smtp_users import AsyncSmtpUsers, SmtpUsers __all__ = [ + # Sync resources "BaseResource", "Email", "Activity", @@ -50,6 +52,33 @@ "SmsRecipients", "SmsWebhooks", "SmsInbounds", + "SmtpUsers", "Other", "DmarcMonitoring", + # Async resources + "AsyncBaseResource", + "AsyncEmail", + "AsyncActivity", + "AsyncAnalytics", + "AsyncDomains", + "AsyncIdentitiesResource", + "AsyncInboundResource", + "AsyncMessages", + "AsyncSchedules", + "AsyncRecipients", + "AsyncTemplates", + "AsyncTokens", + "AsyncWebhooks", + "AsyncEmailVerification", + "AsyncUsers", + "AsyncSmsMessages", + "AsyncSmsNumbers", + "AsyncSmsActivity", + "AsyncSmsSending", + "AsyncSmsRecipients", + "AsyncSmsWebhooks", + "AsyncSmsInbounds", + "AsyncSmtpUsers", + "AsyncOther", + "AsyncDmarcMonitoring", ] diff --git a/mailersend/resources/activity.py b/mailersend/resources/activity.py index 45037bd..5752ef1 100644 --- a/mailersend/resources/activity.py +++ b/mailersend/resources/activity.py @@ -1,6 +1,6 @@ """Activity resource""" -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.activity import ActivityRequest, SingleActivityRequest from ..models.base import APIResponse @@ -52,3 +52,40 @@ def get_single(self, request: SingleActivityRequest) -> APIResponse: ) return self._create_response(response) + + +class AsyncActivity(AsyncBaseResource): + """Async client for interacting with the MailerSend Activity API.""" + + async def get(self, request: ActivityRequest) -> APIResponse: + """ + Get activity data for a domain. + + Args: + request: A fully-validated ActivityRequest object + + Returns: + APIResponse with activity data and metadata + """ + self.logger.debug("Preparing to get activity data") + params = request.to_query_params() + response = await self.client.request( + method="GET", path=f"activity/{request.domain_id}", params=params + ) + return self._create_response(response) + + async def get_single(self, request: SingleActivityRequest) -> APIResponse: + """ + Get a single activity by its ID. + + Args: + request: A fully-validated SingleActivityRequest object + + Returns: + APIResponse with single activity data + """ + self.logger.debug("Getting single activity: %s", request.activity_id) + response = await self.client.request( + method="GET", path=f"activities/{request.activity_id}" + ) + return self._create_response(response) diff --git a/mailersend/resources/analytics.py b/mailersend/resources/analytics.py index 9745fa2..716b4f3 100644 --- a/mailersend/resources/analytics.py +++ b/mailersend/resources/analytics.py @@ -2,7 +2,7 @@ from typing import Dict, Any, Optional -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.analytics import AnalyticsRequest from ..models.base import APIResponse @@ -129,3 +129,75 @@ def _build_query_params( params.pop(f"{field}[]", None) return params + + +class AsyncAnalytics(AsyncBaseResource): + """Async client for interacting with the MailerSend Analytics API.""" + + async def get_activity_by_date(self, request: AnalyticsRequest) -> APIResponse: + """ + Retrieve analytics data grouped by date. + + Args: + request: AnalyticsRequest with date range and filtering options + + Returns: + APIResponse with activity data grouped by date + """ + params = self._build_query_params(request) + response = await self.client.request("GET", "analytics/date", params=params) + return self._create_response(response) + + async def get_opens_by_country(self, request: AnalyticsRequest) -> APIResponse: + """ + Retrieve analytics data grouped by country. + + Args: + request: AnalyticsRequest with date range and filtering options + + Returns: + APIResponse with opens data grouped by country + """ + params = self._build_query_params(request, exclude_fields=["event", "group_by"]) + response = await self.client.request("GET", "analytics/country", params=params) + return self._create_response(response) + + async def get_opens_by_user_agent(self, request: AnalyticsRequest) -> APIResponse: + """ + Retrieve analytics data grouped by user agent name. + + Args: + request: AnalyticsRequest with date range and filtering options + + Returns: + APIResponse with opens data grouped by user agent + """ + params = self._build_query_params(request, exclude_fields=["event", "group_by"]) + response = await self.client.request("GET", "analytics/ua-name", params=params) + return self._create_response(response) + + async def get_opens_by_reading_environment( + self, request: AnalyticsRequest + ) -> APIResponse: + """ + Retrieve analytics data grouped by reading environment. + + Args: + request: AnalyticsRequest with date range and filtering options + + Returns: + APIResponse with opens data grouped by reading environment + """ + params = self._build_query_params(request, exclude_fields=["event", "group_by"]) + response = await self.client.request("GET", "analytics/ua-type", params=params) + return self._create_response(response) + + def _build_query_params( + self, request: AnalyticsRequest, exclude_fields: Optional[list] = None + ) -> Dict[str, Any]: + exclude_fields = exclude_fields or [] + params = request.model_dump(by_alias=True, exclude_none=True) + for field in exclude_fields: + params.pop(field, None) + params.pop(f"{field}[]", None) + return params diff --git a/mailersend/resources/base.py b/mailersend/resources/base.py index 8ddd0fa..ceecbfb 100644 --- a/mailersend/resources/base.py +++ b/mailersend/resources/base.py @@ -1,8 +1,7 @@ import logging -from typing import Dict, Any, Optional, Union, List, TypeVar, Type, ClassVar +from typing import Any, Dict, Optional, Union, List, TypeVar, Type, ClassVar from ..models.base import BaseModel, ModelList, APIResponse from ..logging import get_logger -import requests T = TypeVar("T", bound=BaseModel) @@ -23,9 +22,7 @@ def __init__(self, client, logger: Optional[logging.Logger] = None): self.client = client self.logger = logger or get_logger() - def _create_response( - self, response: requests.Response, data: Any = None - ) -> APIResponse: + def _create_response(self, response: Any, data: Any = None) -> APIResponse: """ Create unified APIResponse object from HTTP response. @@ -53,9 +50,7 @@ def _create_response( ), ) - def _parse_int_header( - self, response: requests.Response, header: str - ) -> Optional[int]: + def _parse_int_header(self, response: Any, header: str) -> Optional[int]: """ Safely parse integer header value. @@ -110,3 +105,9 @@ def _process_response( return [cls(**item) for item in response_data] return response_data + + +class AsyncBaseResource(BaseResource): + """Base class for all async API resources.""" + + pass diff --git a/mailersend/resources/dmarc_monitoring.py b/mailersend/resources/dmarc_monitoring.py index aa63f3a..2b0a23f 100644 --- a/mailersend/resources/dmarc_monitoring.py +++ b/mailersend/resources/dmarc_monitoring.py @@ -2,7 +2,7 @@ from typing import Optional -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.base import APIResponse from ..models.dmarc_monitoring import ( DmarcMonitoringListRequest, @@ -216,3 +216,177 @@ def remove_ip_favorite( path=f"dmarc-monitoring/{request.monitor_id}/favorite/{request.ip}", ) return self._create_response(response) + + +class AsyncDmarcMonitoring(AsyncBaseResource): + """Async client for the MailerSend DMARC Monitoring API.""" + + async def list_monitors( + self, request: Optional[DmarcMonitoringListRequest] = None + ) -> APIResponse: + """ + Retrieve a list of DMARC monitors. + + Args: + request: Optional DmarcMonitoringListRequest with pagination options + + Returns: + APIResponse with list of monitors + """ + if request is None: + request = DmarcMonitoringListRequest( + query_params=DmarcMonitoringListQueryParams() + ) + params = request.to_query_params() + response = await self.client.request( + method="GET", path="dmarc-monitoring", params=params + ) + return self._create_response(response) + + async def create_monitor( + self, request: DmarcMonitoringCreateRequest + ) -> APIResponse: + """ + Create a new DMARC monitor. + + Args: + request: DmarcMonitoringCreateRequest with domain_id + + Returns: + APIResponse with created monitor information + """ + body = request.model_dump(by_alias=True, exclude_none=True) + response = await self.client.request( + method="POST", path="dmarc-monitoring", body=body + ) + return self._create_response(response) + + async def update_monitor( + self, request: DmarcMonitoringUpdateRequest + ) -> APIResponse: + """ + Update a DMARC monitor. + + Args: + request: DmarcMonitoringUpdateRequest with monitor_id and wanted_dmarc_record + + Returns: + APIResponse with updated monitor information + """ + body = request.model_dump( + by_alias=True, exclude_none=True, exclude={"monitor_id"} + ) + response = await self.client.request( + method="PUT", path=f"dmarc-monitoring/{request.monitor_id}", body=body + ) + return self._create_response(response) + + async def delete_monitor( + self, request: DmarcMonitoringDeleteRequest + ) -> APIResponse: + """ + Delete a DMARC monitor. + + Args: + request: DmarcMonitoringDeleteRequest with monitor_id + + Returns: + APIResponse + """ + response = await self.client.request( + method="DELETE", path=f"dmarc-monitoring/{request.monitor_id}" + ) + return self._create_response(response) + + async def get_aggregated_report( + self, request: DmarcMonitoringReportRequest + ) -> APIResponse: + """ + Get aggregated DMARC reports for a monitor. + + Args: + request: DmarcMonitoringReportRequest with monitor_id and pagination options + + Returns: + APIResponse with aggregated report data + """ + params = request.to_query_params() + response = await self.client.request( + method="GET", + path=f"dmarc-monitoring/{request.monitor_id}/report", + params=params, + ) + return self._create_response(response) + + async def get_ip_report( + self, request: DmarcMonitoringIpReportRequest + ) -> APIResponse: + """ + Get IP-specific DMARC reports for a monitor. + + Args: + request: DmarcMonitoringIpReportRequest with monitor_id, ip, and pagination options + + Returns: + APIResponse with IP-specific report data + """ + params = request.to_query_params() + response = await self.client.request( + method="GET", + path=f"dmarc-monitoring/{request.monitor_id}/report/{request.ip}", + params=params, + ) + return self._create_response(response) + + async def get_report_sources( + self, request: DmarcMonitoringReportSourcesRequest + ) -> APIResponse: + """ + Get report sources for a DMARC monitor. + + Args: + request: DmarcMonitoringReportSourcesRequest with monitor_id + + Returns: + APIResponse with report sources data + """ + response = await self.client.request( + method="GET", path=f"dmarc-monitoring/{request.monitor_id}/report-sources" + ) + return self._create_response(response) + + async def mark_ip_favorite( + self, request: DmarcMonitoringFavoriteRequest + ) -> APIResponse: + """ + Mark an IP address as favorite for a DMARC monitor. + + Args: + request: DmarcMonitoringFavoriteRequest with monitor_id and ip + + Returns: + APIResponse + """ + response = await self.client.request( + method="PUT", + path=f"dmarc-monitoring/{request.monitor_id}/favorite/{request.ip}", + ) + return self._create_response(response) + + async def remove_ip_favorite( + self, request: DmarcMonitoringFavoriteRequest + ) -> APIResponse: + """ + Remove an IP address from favorites for a DMARC monitor. + + Args: + request: DmarcMonitoringFavoriteRequest with monitor_id and ip + + Returns: + APIResponse + """ + response = await self.client.request( + method="DELETE", + path=f"dmarc-monitoring/{request.monitor_id}/favorite/{request.ip}", + ) + return self._create_response(response) diff --git a/mailersend/resources/domains.py b/mailersend/resources/domains.py index db30010..4ae2a35 100644 --- a/mailersend/resources/domains.py +++ b/mailersend/resources/domains.py @@ -2,7 +2,7 @@ from typing import Optional -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.domains import ( DomainListRequest, DomainCreateRequest, @@ -202,3 +202,145 @@ def get_domain_verification_status( ) return self._create_response(response) + + +class AsyncDomains(AsyncBaseResource): + """Async client for interacting with the MailerSend Domains API.""" + + async def list_domains( + self, request: Optional[DomainListRequest] = None + ) -> APIResponse: + """ + Retrieve a list of domains. + + Args: + request: Optional DomainListRequest with filtering and pagination options + + Returns: + APIResponse with list of domains + """ + if not request: + query_params = DomainListQueryParams() + params = query_params.to_query_params() + else: + params = request.to_query_params() + response = await self.client.request( + method="GET", path="domains", params=params + ) + return self._create_response(response) + + async def get_domain(self, request: DomainGetRequest) -> APIResponse: + """ + Retrieve information about a single domain. + + Args: + request: DomainGetRequest with domain ID + + Returns: + APIResponse with domain information + """ + response = await self.client.request( + method="GET", path=f"domains/{request.domain_id}" + ) + return self._create_response(response) + + async def create_domain(self, request: DomainCreateRequest) -> APIResponse: + """ + Create a new domain. + + Args: + request: DomainCreateRequest with domain creation details + + Returns: + APIResponse with created domain information + """ + body = request.model_dump(by_alias=True, exclude_none=True) + response = await self.client.request(method="POST", path="domains", body=body) + return self._create_response(response) + + async def delete_domain(self, request: DomainDeleteRequest) -> APIResponse: + """ + Delete a domain. + + Args: + request: DomainDeleteRequest with domain ID to delete + + Returns: + APIResponse (204 No Content on success) + """ + response = await self.client.request( + method="DELETE", path=f"domains/{request.domain_id}" + ) + return self._create_response(response) + + async def get_domain_recipients( + self, request: DomainRecipientsRequest + ) -> APIResponse: + """ + Retrieve recipients for a domain. + + Args: + request: DomainRecipientsRequest with domain ID and pagination options + + Returns: + APIResponse with list of domain recipients + """ + params = request.to_query_params() + response = await self.client.request( + method="GET", path=f"domains/{request.domain_id}/recipients", params=params + ) + return self._create_response(response) + + async def update_domain_settings( + self, request: DomainUpdateSettingsRequest + ) -> APIResponse: + """ + Update domain settings. + + Args: + request: DomainUpdateSettingsRequest with domain ID and settings to update + + Returns: + APIResponse with updated domain information + """ + body = request.model_dump( + by_alias=True, exclude_none=True, exclude={"domain_id"} + ) + response = await self.client.request( + method="PUT", path=f"domains/{request.domain_id}/settings", body=body + ) + return self._create_response(response) + + async def get_domain_dns_records( + self, request: DomainDnsRecordsRequest + ) -> APIResponse: + """ + Retrieve DNS records for a domain. + + Args: + request: DomainDnsRecordsRequest with domain ID + + Returns: + APIResponse with domain DNS records + """ + response = await self.client.request( + method="GET", path=f"domains/{request.domain_id}/dns-records" + ) + return self._create_response(response) + + async def get_domain_verification_status( + self, request: DomainVerificationRequest + ) -> APIResponse: + """ + Retrieve verification status for a domain. + + Args: + request: DomainVerificationRequest with domain ID + + Returns: + APIResponse with domain verification status + """ + response = await self.client.request( + method="GET", path=f"domains/{request.domain_id}/verify" + ) + return self._create_response(response) diff --git a/mailersend/resources/email.py b/mailersend/resources/email.py index b3a1531..bd3a11c 100644 --- a/mailersend/resources/email.py +++ b/mailersend/resources/email.py @@ -2,7 +2,7 @@ from typing import List -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.email import EmailRequest from ..models.base import APIResponse @@ -77,3 +77,58 @@ def get_bulk_status(self, bulk_email_id: str) -> APIResponse: response = self.client.request(method="GET", path=f"bulk-email/{bulk_email_id}") return self._create_response(response) + + +class AsyncEmail(AsyncBaseResource): + """Async client for interacting with the MailerSend Email API.""" + + async def send(self, email: EmailRequest) -> APIResponse: + """ + Send a single email. + + Args: + email: A fully-validated EmailRequest object + + Returns: + APIResponse with email ID and metadata + """ + self.logger.debug("Preparing to send email") + payload = email.model_dump(by_alias=True, exclude_none=True) + self.logger.debug("Sending email request to MailerSend API") + response = await self.client.request(method="POST", path="email", body=payload) + email_data = {"id": response.headers.get("x-message-id")} + return self._create_response(response, email_data) + + async def send_bulk(self, emails: List[EmailRequest]) -> APIResponse: + """ + Send multiple emails in one request. + + Args: + emails: List of EmailRequest objects to send + + Returns: + APIResponse with bulk email information and metadata + """ + self.logger.debug("Preparing to send emails in bulk") + payload = [e.model_dump(by_alias=True, exclude_none=True) for e in emails] + self.logger.debug("Sending bulk email request to MailerSend API") + response = await self.client.request( + method="POST", path="bulk-email", body=payload + ) + return self._create_response(response) + + async def get_bulk_status(self, bulk_email_id: str) -> APIResponse: + """ + Get the status of a bulk email send request. + + Args: + bulk_email_id: The ID of the bulk email request + + Returns: + APIResponse with bulk email status and metadata + """ + self.logger.debug("Getting bulk email status") + response = await self.client.request( + method="GET", path=f"bulk-email/{bulk_email_id}" + ) + return self._create_response(response) diff --git a/mailersend/resources/email_verification.py b/mailersend/resources/email_verification.py index d31734c..2a2b5ca 100644 --- a/mailersend/resources/email_verification.py +++ b/mailersend/resources/email_verification.py @@ -1,6 +1,6 @@ """Email Verification resource""" -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.base import APIResponse from ..models.email_verification import ( EmailVerifyRequest, @@ -225,3 +225,138 @@ def get_results(self, request: EmailVerificationResultsRequest) -> APIResponse: # Create standardized response return self._create_response(response) + + +class AsyncEmailVerification(AsyncBaseResource): + """Async resource for managing email verification.""" + + async def verify_email(self, request: EmailVerifyRequest) -> APIResponse: + """Verify a single email address (synchronous). + + Args: + request: The email verification request data. + + Returns: + APIResponse with verification result + """ + body = request.model_dump(exclude_none=True) + response = await self.client.request( + method="POST", path="email-verification/verify", body=body + ) + return self._create_response(response) + + async def verify_email_async(self, request: EmailVerifyAsyncRequest) -> APIResponse: + """Verify a single email address (asynchronous). + + Args: + request: The async email verification request data. + + Returns: + APIResponse with verification result + """ + body = request.model_dump(exclude_none=True) + response = await self.client.request( + method="POST", path="email-verification/verify-async", body=body + ) + return self._create_response(response) + + async def get_async_status( + self, request: EmailVerificationAsyncStatusRequest + ) -> APIResponse: + """Get the status of an async email verification. + + Args: + request: The async status request data. + + Returns: + APIResponse with EmailVerificationAsyncStatusResponse data + """ + response = await self.client.request( + method="GET", + path=f"email-verification/verify-async/{request.email_verification_id}", + ) + return self._create_response(response) + + async def list_verifications( + self, request: EmailVerificationListsRequest + ) -> APIResponse: + """List all email verification lists. + + Args: + request: The list request data with pagination options. + + Returns: + APIResponse with list of email verification lists + """ + params = request.to_query_params() + response = await self.client.request( + method="GET", path="email-verification", params=params + ) + return self._create_response(response) + + async def get_verification( + self, request: EmailVerificationGetRequest + ) -> APIResponse: + """Get a single email verification list. + + Args: + request: The get verification request data. + + Returns: + APIResponse with email verification list data + """ + response = await self.client.request( + method="GET", path=f"email-verification/{request.email_verification_id}" + ) + return self._create_response(response) + + async def create_verification( + self, request: EmailVerificationCreateRequest + ) -> APIResponse: + """Create a new email verification list. + + Args: + request: The create verification request data. + + Returns: + APIResponse with email verification list data + """ + body = request.model_dump(exclude_none=True) + response = await self.client.request( + method="POST", path="email-verification", body=body + ) + return self._create_response(response) + + async def verify_list(self, request: EmailVerificationVerifyRequest) -> APIResponse: + """Start verification of an email verification list. + + Args: + request: The verify list request data. + + Returns: + APIResponse with verification result + """ + response = await self.client.request( + method="GET", + path=f"email-verification/{request.email_verification_id}/verify", + ) + return self._create_response(response) + + async def get_results( + self, request: EmailVerificationResultsRequest + ) -> APIResponse: + """Get verification results for an email verification list. + + Args: + request: The results request data with optional filters. + + Returns: + APIResponse with verification results + """ + params = request.to_query_params() + response = await self.client.request( + method="GET", + path=f"email-verification/{request.email_verification_id}/results", + params=params, + ) + return self._create_response(response) diff --git a/mailersend/resources/identities.py b/mailersend/resources/identities.py index 78424ae..0a28f9a 100644 --- a/mailersend/resources/identities.py +++ b/mailersend/resources/identities.py @@ -11,7 +11,7 @@ IdentityDeleteByEmailRequest, ) from ..models.base import APIResponse -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource class IdentitiesResource(BaseResource): @@ -211,3 +211,143 @@ def delete_identity_by_email( ) return self._create_response(response) + + +class AsyncIdentitiesResource(AsyncBaseResource): + """Async resource for managing sender identities.""" + + async def list_identities(self, request: IdentityListRequest) -> APIResponse: + """ + Get a list of sender identities. + + Args: + request: The identity list request containing filtering and pagination parameters + + Returns: + APIResponse containing the identities list response + """ + params = request.to_query_params() + response = await self.client.request( + method="GET", path="identities", params=params if params else None + ) + return self._create_response(response) + + async def create_identity(self, request: IdentityCreateRequest) -> APIResponse: + """ + Create a new sender identity. + + Args: + request: The identity creation request with all required data + + Returns: + APIResponse containing the created identity response + """ + data = request.model_dump(by_alias=True, exclude_none=True) + response = await self.client.request( + method="POST", path="identities", body=data + ) + return self._create_response(response) + + async def get_identity(self, request: IdentityGetRequest) -> APIResponse: + """ + Get a single sender identity by ID. + + Args: + request: The identity get request with identity ID + + Returns: + APIResponse containing the identity data + """ + response = await self.client.request( + method="GET", path=f"identities/{request.identity_id}" + ) + return self._create_response(response) + + async def get_identity_by_email( + self, request: IdentityGetByEmailRequest + ) -> APIResponse: + """ + Get a single sender identity by email. + + Args: + request: The identity get by email request + + Returns: + APIResponse containing the identity data + """ + response = await self.client.request( + method="GET", path=f"identities/email/{request.email}" + ) + return self._create_response(response) + + async def update_identity(self, request: IdentityUpdateRequest) -> APIResponse: + """ + Update a sender identity by ID. + + Args: + request: The identity update request with identity ID and update data + + Returns: + APIResponse containing the updated identity + """ + data = request.model_dump( + by_alias=True, exclude_none=True, exclude={"identity_id"} + ) + response = await self.client.request( + method="PUT", + path=f"identities/{request.identity_id}", + body=data if data else None, + ) + return self._create_response(response) + + async def update_identity_by_email( + self, request: IdentityUpdateByEmailRequest + ) -> APIResponse: + """ + Update a sender identity by email. + + Args: + request: The identity update by email request + + Returns: + APIResponse containing the updated identity + """ + data = request.model_dump(by_alias=True, exclude_none=True, exclude={"email"}) + response = await self.client.request( + method="PUT", + path=f"identities/email/{request.email}", + body=data if data else None, + ) + return self._create_response(response) + + async def delete_identity(self, request: IdentityDeleteRequest) -> APIResponse: + """ + Delete a sender identity by ID. + + Args: + request: The identity delete request with identity ID + + Returns: + APIResponse containing the deletion result + """ + response = await self.client.request( + method="DELETE", path=f"identities/{request.identity_id}" + ) + return self._create_response(response) + + async def delete_identity_by_email( + self, request: IdentityDeleteByEmailRequest + ) -> APIResponse: + """ + Delete a sender identity by email. + + Args: + request: The identity delete by email request + + Returns: + APIResponse containing the deletion result + """ + response = await self.client.request( + method="DELETE", path=f"identities/email/{request.email}" + ) + return self._create_response(response) diff --git a/mailersend/resources/inbound.py b/mailersend/resources/inbound.py index 0c1a5f1..c0d94cd 100644 --- a/mailersend/resources/inbound.py +++ b/mailersend/resources/inbound.py @@ -8,7 +8,7 @@ InboundDeleteRequest, ) from mailersend.models.base import APIResponse -from mailersend.resources.base import BaseResource +from mailersend.resources.base import AsyncBaseResource, BaseResource class InboundResource(BaseResource): @@ -135,3 +135,83 @@ def delete(self, request: InboundDeleteRequest) -> APIResponse: ) return self._create_response(response) + + +class AsyncInboundResource(AsyncBaseResource): + """Async resource for managing inbound routes.""" + + async def list(self, request: InboundListRequest) -> APIResponse: + """ + Get a list of inbound routes. + + Args: + request: The inbound list request containing filtering and pagination parameters + + Returns: + APIResponse containing the inbound routes list response + """ + params = request.to_query_params() + response = await self.client.request( + method="GET", path="inbound", params=params if params else None + ) + return self._create_response(response) + + async def get(self, request: InboundGetRequest) -> APIResponse: + """ + Get a single inbound route by ID. + + Args: + request: The inbound get request with inbound ID + + Returns: + APIResponse containing the inbound route data + """ + response = await self.client.request( + method="GET", path=f"inbound/{request.inbound_id}" + ) + return self._create_response(response) + + async def create(self, request: InboundCreateRequest) -> APIResponse: + """ + Create a new inbound route. + + Args: + request: The inbound create request with all required data + + Returns: + APIResponse containing the created inbound route response + """ + data = request.to_request_body() + response = await self.client.request(method="POST", path="inbound", body=data) + return self._create_response(response) + + async def update(self, request: InboundUpdateRequest) -> APIResponse: + """ + Update an existing inbound route. + + Args: + request: The inbound update request with inbound ID and update data + + Returns: + APIResponse containing the updated response + """ + data = request.to_request_body() + response = await self.client.request( + method="PUT", path=f"inbound/{request.inbound_id}", body=data + ) + return self._create_response(response) + + async def delete(self, request: InboundDeleteRequest) -> APIResponse: + """ + Delete an inbound route. + + Args: + request: The inbound delete request with inbound ID + + Returns: + APIResponse containing the deletion result + """ + response = await self.client.request( + method="DELETE", path=f"inbound/{request.inbound_id}" + ) + return self._create_response(response) diff --git a/mailersend/resources/messages.py b/mailersend/resources/messages.py index 220c1a8..b69aab3 100644 --- a/mailersend/resources/messages.py +++ b/mailersend/resources/messages.py @@ -1,6 +1,6 @@ """Messages resource""" -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.messages import ( MessagesListRequest, MessageGetRequest, @@ -57,3 +57,38 @@ def get_message(self, request: MessageGetRequest) -> APIResponse: ) return self._create_response(response) + + +class AsyncMessages(AsyncBaseResource): + """Async client for interacting with the MailerSend Messages API.""" + + async def list_messages(self, request: MessagesListRequest) -> APIResponse: + """ + Retrieve a list of messages. + + Args: + request: MessagesListRequest with pagination options + + Returns: + APIResponse containing the messages list response + """ + params = request.to_query_params() + response = await self.client.request( + method="GET", path="messages", params=params if params else None + ) + return self._create_response(response) + + async def get_message(self, request: MessageGetRequest) -> APIResponse: + """ + Retrieve information about a single message. + + Args: + request: MessageGetRequest with message ID + + Returns: + APIResponse containing the message response + """ + response = await self.client.request( + method="GET", path=f"messages/{request.message_id}" + ) + return self._create_response(response) diff --git a/mailersend/resources/other.py b/mailersend/resources/other.py index 27fa586..587d8fd 100644 --- a/mailersend/resources/other.py +++ b/mailersend/resources/other.py @@ -1,6 +1,6 @@ """Other endpoints resource""" -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.base import APIResponse @@ -23,3 +23,17 @@ def get_quota(self) -> APIResponse: response = self.client.request(method="GET", path="api-quota") return self._create_response(response) + + +class AsyncOther(AsyncBaseResource): + """Async client for other MailerSend API endpoints.""" + + async def get_quota(self) -> APIResponse: + """ + Get API quota information. + + Returns: + APIResponse with quota information including remaining requests + """ + response = await self.client.request(method="GET", path="api-quota") + return self._create_response(response) diff --git a/mailersend/resources/recipients.py b/mailersend/resources/recipients.py index f881d4a..f0d6a38 100644 --- a/mailersend/resources/recipients.py +++ b/mailersend/resources/recipients.py @@ -13,7 +13,7 @@ RecipientsListQueryParams, SuppressionListQueryParams, ) -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource class Recipients(BaseResource): @@ -425,3 +425,311 @@ def delete_from_on_hold(self, request: SuppressionDeleteRequest) -> APIResponse: ) return self._create_response(response) + + +class AsyncRecipients(AsyncBaseResource): + """Async Recipients API resource.""" + + async def list_recipients( + self, request: Optional[RecipientsListRequest] = None + ) -> APIResponse: + """ + List recipients with optional filtering. + + Args: + request: Request parameters for listing recipients (optional) + + Returns: + APIResponse with recipients list + """ + if request is None: + request = RecipientsListRequest(query_params=RecipientsListQueryParams()) + params = request.to_query_params() + response = await self.client.request( + method="GET", path="recipients", params=params + ) + return self._create_response(response) + + async def get_recipient(self, request: RecipientGetRequest) -> APIResponse: + """ + Get a single recipient by ID. + + Args: + request: Request parameters for getting recipient + + Returns: + APIResponse with recipient data + """ + response = await self.client.request( + method="GET", path=f"recipients/{request.recipient_id}" + ) + return self._create_response(response) + + async def delete_recipient(self, request: RecipientDeleteRequest) -> APIResponse: + """ + Delete a recipient. + + Args: + request: Request parameters for deleting recipient + + Returns: + APIResponse with empty data + """ + response = await self.client.request( + method="DELETE", path=f"recipients/{request.recipient_id}" + ) + return self._create_response(response) + + async def list_blocklist( + self, request: Optional[SuppressionListRequest] = None + ) -> APIResponse: + """ + List blocklist entries. + + Args: + request: Request parameters for listing blocklist entries (optional) + + Returns: + APIResponse with blocklist entries + """ + if request is None: + request = SuppressionListRequest(query_params=SuppressionListQueryParams()) + params = request.to_query_params() + response = await self.client.request( + method="GET", path="suppressions/blocklist", params=params + ) + return self._create_response(response) + + async def list_hard_bounces( + self, request: Optional[SuppressionListRequest] = None + ) -> APIResponse: + """ + List hard bounces. + + Args: + request: Request parameters for listing hard bounces (optional) + + Returns: + APIResponse with hard bounces + """ + if request is None: + request = SuppressionListRequest(query_params=SuppressionListQueryParams()) + params = request.to_query_params() + response = await self.client.request( + method="GET", path="suppressions/hard-bounces", params=params + ) + return self._create_response(response) + + async def list_spam_complaints( + self, request: Optional[SuppressionListRequest] = None + ) -> APIResponse: + """ + List spam complaints. + + Args: + request: Request parameters for listing spam complaints (optional) + + Returns: + APIResponse with spam complaints + """ + if request is None: + request = SuppressionListRequest(query_params=SuppressionListQueryParams()) + params = request.to_query_params() + response = await self.client.request( + method="GET", path="suppressions/spam-complaints", params=params + ) + return self._create_response(response) + + async def list_unsubscribes( + self, request: Optional[SuppressionListRequest] = None + ) -> APIResponse: + """ + List unsubscribes. + + Args: + request: Request parameters for listing unsubscribes (optional) + + Returns: + APIResponse with unsubscribes + """ + if request is None: + request = SuppressionListRequest(query_params=SuppressionListQueryParams()) + params = request.to_query_params() + response = await self.client.request( + method="GET", path="suppressions/unsubscribes", params=params + ) + return self._create_response(response) + + async def list_on_hold( + self, request: Optional[SuppressionListRequest] = None + ) -> APIResponse: + """ + List on-hold entries. + + Args: + request: Request parameters for listing on-hold entries (optional) + + Returns: + APIResponse with on-hold entries + """ + if request is None: + request = SuppressionListRequest(query_params=SuppressionListQueryParams()) + params = request.to_query_params() + response = await self.client.request( + method="GET", path="suppressions/on-hold-list", params=params + ) + return self._create_response(response) + + async def add_to_blocklist(self, request: SuppressionAddRequest) -> APIResponse: + """ + Add entries to blocklist. + + Args: + request: Request parameters for adding to blocklist + + Returns: + APIResponse with added entries + """ + body = request.model_dump(by_alias=True, exclude_none=True) + response = await self.client.request( + method="POST", path="suppressions/blocklist", body=body + ) + return self._create_response(response) + + async def add_hard_bounces(self, request: SuppressionAddRequest) -> APIResponse: + """ + Add hard bounces. + + Args: + request: Request parameters for adding hard bounces + + Returns: + APIResponse with added entries + """ + body = request.model_dump(by_alias=True, exclude_none=True) + response = await self.client.request( + method="POST", path="suppressions/hard-bounces", body=body + ) + return self._create_response(response) + + async def add_spam_complaints(self, request: SuppressionAddRequest) -> APIResponse: + """ + Add spam complaints. + + Args: + request: Request parameters for adding spam complaints + + Returns: + APIResponse with added entries + """ + body = request.model_dump(by_alias=True, exclude_none=True) + response = await self.client.request( + method="POST", path="suppressions/spam-complaints", body=body + ) + return self._create_response(response) + + async def add_unsubscribes(self, request: SuppressionAddRequest) -> APIResponse: + """ + Add unsubscribes. + + Args: + request: Request parameters for adding unsubscribes + + Returns: + APIResponse with added entries + """ + body = request.model_dump(by_alias=True, exclude_none=True) + response = await self.client.request( + method="POST", path="suppressions/unsubscribes", body=body + ) + return self._create_response(response) + + async def delete_from_blocklist( + self, request: SuppressionDeleteRequest + ) -> APIResponse: + """ + Delete entries from blocklist. + + Args: + request: Request parameters for deleting from blocklist + + Returns: + APIResponse with deleted entries + """ + body = request.model_dump(by_alias=True, exclude_none=True) + response = await self.client.request( + method="DELETE", path="suppressions/blocklist", body=body + ) + return self._create_response(response) + + async def delete_hard_bounces( + self, request: SuppressionDeleteRequest + ) -> APIResponse: + """ + Delete hard bounces. + + Args: + request: Request parameters for deleting hard bounces + + Returns: + APIResponse with deleted entries + """ + body = request.model_dump(exclude_none=True, exclude={"domain_id"}) + response = await self.client.request( + method="DELETE", path="suppressions/hard-bounces", body=body + ) + return self._create_response(response) + + async def delete_spam_complaints( + self, request: SuppressionDeleteRequest + ) -> APIResponse: + """ + Delete spam complaints. + + Args: + request: Request parameters for deleting spam complaints + + Returns: + APIResponse with deleted entries + """ + body = request.model_dump(exclude_none=True, exclude={"domain_id"}) + response = await self.client.request( + method="DELETE", path="suppressions/spam-complaints", body=body + ) + return self._create_response(response) + + async def delete_unsubscribes( + self, request: SuppressionDeleteRequest + ) -> APIResponse: + """ + Delete unsubscribes. + + Args: + request: Request parameters for deleting unsubscribes + + Returns: + APIResponse with deleted entries + """ + body = request.model_dump(exclude_none=True, exclude={"domain_id"}) + response = await self.client.request( + method="DELETE", path="suppressions/unsubscribes", body=body + ) + return self._create_response(response) + + async def delete_from_on_hold( + self, request: SuppressionDeleteRequest + ) -> APIResponse: + """ + Delete entries from on-hold list. + + Args: + request: Request parameters for deleting from on-hold + + Returns: + APIResponse with deleted entries + """ + body = request.model_dump(exclude_none=True, exclude={"domain_id"}) + response = await self.client.request( + method="DELETE", path="suppressions/on-hold-list", body=body + ) + return self._create_response(response) diff --git a/mailersend/resources/schedules.py b/mailersend/resources/schedules.py index d3dd405..90815e6 100644 --- a/mailersend/resources/schedules.py +++ b/mailersend/resources/schedules.py @@ -1,6 +1,6 @@ """Schedules resource""" -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.schedules import ( SchedulesListRequest, ScheduleGetRequest, @@ -85,3 +85,53 @@ def delete_schedule(self, request: ScheduleDeleteRequest) -> APIResponse: ) return self._create_response(response) + + +class AsyncSchedules(AsyncBaseResource): + """Async client for interacting with the MailerSend Schedules API.""" + + async def list_schedules(self, request: SchedulesListRequest) -> APIResponse: + """ + Retrieve a list of scheduled messages. + + Args: + request: SchedulesListRequest with filtering and pagination options + + Returns: + APIResponse containing the schedules list response + """ + params = request.to_query_params() + response = await self.client.request( + method="GET", path="message-schedules", params=params if params else None + ) + return self._create_response(response) + + async def get_schedule(self, request: ScheduleGetRequest) -> APIResponse: + """ + Retrieve information about a single scheduled message. + + Args: + request: ScheduleGetRequest with message ID + + Returns: + APIResponse containing the schedule response + """ + response = await self.client.request( + method="GET", path=f"message-schedules/{request.message_id}" + ) + return self._create_response(response) + + async def delete_schedule(self, request: ScheduleDeleteRequest) -> APIResponse: + """ + Delete a scheduled message. + + Args: + request: ScheduleDeleteRequest with message ID to delete + + Returns: + APIResponse (204 No Content on success) + """ + response = await self.client.request( + method="DELETE", path=f"message-schedules/{request.message_id}" + ) + return self._create_response(response) diff --git a/mailersend/resources/sms_activity.py b/mailersend/resources/sms_activity.py index a078fc5..cb18980 100644 --- a/mailersend/resources/sms_activity.py +++ b/mailersend/resources/sms_activity.py @@ -2,7 +2,7 @@ SMS Activity API resource. """ -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.sms_activity import SmsActivityListRequest, SmsMessageGetRequest from ..models.base import APIResponse @@ -53,3 +53,38 @@ def get(self, request: SmsMessageGetRequest) -> APIResponse: ) return self._create_response(response) + + +class AsyncSmsActivity(AsyncBaseResource): + """Async resource for SMS Activity API endpoints.""" + + async def list(self, request: SmsActivityListRequest) -> APIResponse: + """ + Get a list of SMS activities. + + Args: + request: SMS activity list request + + Returns: + API response with SMS activities + """ + params = request.to_query_params() + response = await self.client.request( + method="GET", path="sms-activity", params=params + ) + return self._create_response(response) + + async def get(self, request: SmsMessageGetRequest) -> APIResponse: + """ + Get activity of a single SMS message. + + Args: + request: SMS message get request + + Returns: + API response with SMS message activity + """ + response = await self.client.request( + method="GET", path=f"sms-messages/{request.sms_message_id}" + ) + return self._create_response(response) diff --git a/mailersend/resources/sms_inbounds.py b/mailersend/resources/sms_inbounds.py index 9d22afb..15881d6 100644 --- a/mailersend/resources/sms_inbounds.py +++ b/mailersend/resources/sms_inbounds.py @@ -1,6 +1,6 @@ """SMS Inbounds resource.""" -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.sms_inbounds import ( SmsInboundsListRequest, SmsInboundGetRequest, @@ -103,3 +103,80 @@ def delete_sms_inbound(self, request: SmsInboundDeleteRequest) -> APIResponse: ) return self._create_response(response) + + +class AsyncSmsInbounds(AsyncBaseResource): + """Async SMS Inbounds resource.""" + + async def list_sms_inbounds(self, request: SmsInboundsListRequest) -> APIResponse: + """List SMS inbound routes. + + Args: + request: SmsInboundsListRequest with query parameters + + Returns: + APIResponse: Response containing list of SMS inbound routes + """ + params = request.to_query_params() + response = await self.client.request( + method="GET", path="sms-inbounds", params=params + ) + return self._create_response(response) + + async def get_sms_inbound(self, request: SmsInboundGetRequest) -> APIResponse: + """Get a single SMS inbound route. + + Args: + request: SmsInboundGetRequest with inbound ID + + Returns: + APIResponse: Response containing SMS inbound route details + """ + response = await self.client.request( + method="GET", path=f"sms-inbounds/{request.sms_inbound_id}" + ) + return self._create_response(response) + + async def create_sms_inbound(self, request: SmsInboundCreateRequest) -> APIResponse: + """Create a new SMS inbound route. + + Args: + request: SmsInboundCreateRequest with inbound route details + + Returns: + APIResponse: Response containing created SMS inbound route + """ + response = await self.client.request( + method="POST", path="sms-inbounds", body=request.to_request_body() + ) + return self._create_response(response) + + async def update_sms_inbound(self, request: SmsInboundUpdateRequest) -> APIResponse: + """Update an existing SMS inbound route. + + Args: + request: SmsInboundUpdateRequest with inbound ID and updated fields + + Returns: + APIResponse: Response containing updated SMS inbound route + """ + response = await self.client.request( + method="PUT", + path=f"sms-inbounds/{request.sms_inbound_id}", + body=request.to_request_body(), + ) + return self._create_response(response) + + async def delete_sms_inbound(self, request: SmsInboundDeleteRequest) -> APIResponse: + """Delete an SMS inbound route. + + Args: + request: SmsInboundDeleteRequest with inbound ID + + Returns: + APIResponse: Response confirming deletion + """ + response = await self.client.request( + method="DELETE", path=f"sms-inbounds/{request.sms_inbound_id}" + ) + return self._create_response(response) diff --git a/mailersend/resources/sms_messages.py b/mailersend/resources/sms_messages.py index 71fdac1..766283a 100644 --- a/mailersend/resources/sms_messages.py +++ b/mailersend/resources/sms_messages.py @@ -1,6 +1,6 @@ """SMS Messages resource.""" -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.sms_messages import SmsMessagesListRequest, SmsMessageGetRequest from ..models.base import APIResponse @@ -47,3 +47,38 @@ def get_sms_message(self, request: SmsMessageGetRequest) -> APIResponse: ) return self._create_response(response) + + +class AsyncSmsMessages(AsyncBaseResource): + """Async SMS Messages resource.""" + + async def list_sms_messages(self, request: SmsMessagesListRequest) -> APIResponse: + """ + List SMS messages. + + Args: + request: SmsMessagesListRequest object containing query parameters + + Returns: + APIResponse: Response containing list of SMS messages + """ + params = request.to_query_params() + response = await self.client.request( + method="GET", path="sms-messages", params=params + ) + return self._create_response(response) + + async def get_sms_message(self, request: SmsMessageGetRequest) -> APIResponse: + """ + Get a single SMS message. + + Args: + request: SmsMessageGetRequest object containing SMS message ID + + Returns: + APIResponse: Response containing SMS message details + """ + response = await self.client.request( + method="GET", path=f"sms-messages/{request.sms_message_id}" + ) + return self._create_response(response) diff --git a/mailersend/resources/sms_numbers.py b/mailersend/resources/sms_numbers.py index 3bf0410..f7c1153 100644 --- a/mailersend/resources/sms_numbers.py +++ b/mailersend/resources/sms_numbers.py @@ -1,6 +1,6 @@ """SMS Numbers resource""" -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.sms_numbers import ( SmsNumbersListRequest, SmsNumberGetRequest, @@ -94,3 +94,70 @@ def delete(self, request: SmsNumberDeleteRequest) -> APIResponse: ) return self._create_response(response) + + +class AsyncSmsNumbers(AsyncBaseResource): + """Async client for the MailerSend SMS Phone Numbers API.""" + + async def list(self, request: SmsNumbersListRequest) -> APIResponse: + """ + Get a list of SMS phone numbers. + + Args: + request: SmsNumbersListRequest with query parameters + + Returns: + APIResponse with SMS phone numbers list and metadata + """ + params = request.to_query_params() + response = await self.client.request( + method="GET", path="sms-numbers", params=params + ) + return self._create_response(response) + + async def get(self, request: SmsNumberGetRequest) -> APIResponse: + """ + Get a specific SMS phone number. + + Args: + request: SmsNumberGetRequest with SMS number ID + + Returns: + APIResponse with SMS phone number data and metadata + """ + response = await self.client.request( + method="GET", path=f"sms-numbers/{request.sms_number_id}" + ) + return self._create_response(response) + + async def update(self, request: SmsNumberUpdateRequest) -> APIResponse: + """ + Update a specific SMS phone number. + + Args: + request: SmsNumberUpdateRequest with SMS number ID and update data + + Returns: + APIResponse with updated SMS phone number data and metadata + """ + response = await self.client.request( + method="PUT", + path=f"sms-numbers/{request.sms_number_id}", + body=request.to_json(), + ) + return self._create_response(response) + + async def delete(self, request: SmsNumberDeleteRequest) -> APIResponse: + """ + Delete a specific SMS phone number. + + Args: + request: SmsNumberDeleteRequest with SMS number ID + + Returns: + APIResponse with deletion confirmation and metadata + """ + response = await self.client.request( + method="DELETE", path=f"sms-numbers/{request.sms_number_id}" + ) + return self._create_response(response) diff --git a/mailersend/resources/sms_recipients.py b/mailersend/resources/sms_recipients.py index a155675..b716679 100644 --- a/mailersend/resources/sms_recipients.py +++ b/mailersend/resources/sms_recipients.py @@ -1,6 +1,6 @@ """SMS Recipients resource.""" -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.sms_recipients import ( SmsRecipientsListRequest, SmsRecipientGetRequest, @@ -75,3 +75,59 @@ def update_sms_recipient(self, request: SmsRecipientUpdateRequest) -> APIRespons ) return self._create_response(response) + + +class AsyncSmsRecipients(AsyncBaseResource): + """Async SMS Recipients resource.""" + + async def list_sms_recipients( + self, request: SmsRecipientsListRequest + ) -> APIResponse: + """ + List SMS recipients. + + Args: + request: SmsRecipientsListRequest object containing query parameters + + Returns: + APIResponse: Response containing list of SMS recipients + """ + params = request.to_query_params() + response = await self.client.request( + method="GET", path="sms-recipients", params=params + ) + return self._create_response(response) + + async def get_sms_recipient(self, request: SmsRecipientGetRequest) -> APIResponse: + """ + Get a single SMS recipient. + + Args: + request: SmsRecipientGetRequest object containing SMS recipient ID + + Returns: + APIResponse: Response containing SMS recipient details + """ + response = await self.client.request( + method="GET", path=f"sms-recipients/{request.sms_recipient_id}" + ) + return self._create_response(response) + + async def update_sms_recipient( + self, request: SmsRecipientUpdateRequest + ) -> APIResponse: + """ + Update a single SMS recipient. + + Args: + request: SmsRecipientUpdateRequest object containing SMS recipient ID and new status + + Returns: + APIResponse: Response containing updated SMS recipient + """ + response = await self.client.request( + method="PUT", + path=f"sms-recipients/{request.sms_recipient_id}", + body=request.to_request_body(), + ) + return self._create_response(response) diff --git a/mailersend/resources/sms_sending.py b/mailersend/resources/sms_sending.py index 9a1998c..fc97f63 100644 --- a/mailersend/resources/sms_sending.py +++ b/mailersend/resources/sms_sending.py @@ -1,6 +1,6 @@ """SMS Sending resource""" -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.sms_sending import SmsSendRequest from ..models.base import APIResponse @@ -30,3 +30,21 @@ def send(self, request: SmsSendRequest) -> APIResponse: response = self.client.request(method="POST", path="sms", body=payload) return self._create_response(response) + + +class AsyncSmsSending(AsyncBaseResource): + """Async client for the MailerSend SMS Sending API.""" + + async def send(self, request: SmsSendRequest) -> APIResponse: + """ + Send an SMS message. + + Args: + request: SmsSendRequest with SMS details + + Returns: + APIResponse with SMS sending response and metadata + """ + payload = request.to_json() + response = await self.client.request(method="POST", path="sms", body=payload) + return self._create_response(response) diff --git a/mailersend/resources/sms_webhooks.py b/mailersend/resources/sms_webhooks.py index 63dceb0..219f4d2 100644 --- a/mailersend/resources/sms_webhooks.py +++ b/mailersend/resources/sms_webhooks.py @@ -1,6 +1,6 @@ """SMS Webhooks resource.""" -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.sms_webhooks import ( SmsWebhooksListRequest, SmsWebhookGetRequest, @@ -111,3 +111,83 @@ def delete_sms_webhook(self, request: SmsWebhookDeleteRequest) -> APIResponse: ) return self._create_response(response) + + +class AsyncSmsWebhooks(AsyncBaseResource): + """Async SMS Webhooks resource.""" + + async def list_sms_webhooks(self, request: SmsWebhooksListRequest) -> APIResponse: + """List SMS webhooks. + + Args: + request: SmsWebhooksListRequest object containing query parameters + + Returns: + APIResponse: Response containing list of SMS webhooks + """ + params = request.to_query_params() + response = await self.client.request( + method="GET", path="sms-webhooks", params=params + ) + return self._create_response(response) + + async def get_sms_webhook(self, request: SmsWebhookGetRequest) -> APIResponse: + """Get a single SMS webhook. + + Args: + request: SmsWebhookGetRequest object containing SMS webhook ID + + Returns: + APIResponse: Response containing SMS webhook details + """ + response = await self.client.request( + method="GET", path=f"sms-webhooks/{request.sms_webhook_id}" + ) + return self._create_response(response) + + async def create_sms_webhook(self, request: SmsWebhookCreateRequest) -> APIResponse: + """ + Create an SMS webhook. + + Args: + request: SmsWebhookCreateRequest object containing webhook data + + Returns: + APIResponse: Response containing created SMS webhook + """ + response = await self.client.request( + method="POST", path="sms-webhooks", body=request.to_request_body() + ) + return self._create_response(response) + + async def update_sms_webhook(self, request: SmsWebhookUpdateRequest) -> APIResponse: + """ + Update an SMS webhook. + + Args: + request: SmsWebhookUpdateRequest object containing SMS webhook ID and update data + + Returns: + APIResponse: Response containing updated SMS webhook + """ + response = await self.client.request( + method="PUT", + path=f"sms-webhooks/{request.sms_webhook_id}", + body=request.to_request_body(), + ) + return self._create_response(response) + + async def delete_sms_webhook(self, request: SmsWebhookDeleteRequest) -> APIResponse: + """ + Delete an SMS webhook. + + Args: + request: SmsWebhookDeleteRequest object containing SMS webhook ID + + Returns: + APIResponse: Response confirming deletion + """ + response = await self.client.request( + method="DELETE", path=f"sms-webhooks/{request.sms_webhook_id}" + ) + return self._create_response(response) diff --git a/mailersend/resources/smtp_users.py b/mailersend/resources/smtp_users.py index 46727b8..a5de7f4 100644 --- a/mailersend/resources/smtp_users.py +++ b/mailersend/resources/smtp_users.py @@ -1,6 +1,6 @@ """SMTP Users API resource.""" -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.base import APIResponse from ..models.smtp_users import ( SmtpUsersListRequest, @@ -137,3 +137,84 @@ def delete_smtp_user(self, request: SmtpUserDeleteRequest) -> APIResponse: # Create standardized response return self._create_response(response) + + +class AsyncSmtpUsers(AsyncBaseResource): + """Async SMTP Users API resource.""" + + async def list_smtp_users(self, request: SmtpUsersListRequest) -> APIResponse: + """List SMTP users for a domain. + + Args: + request: The list SMTP users request + + Returns: + APIResponse: API response with SMTP users list data + """ + params = request.to_query_params() + response = await self.client.request( + method="GET", path=f"domains/{request.domain_id}/smtp-users", params=params + ) + return self._create_response(response) + + async def get_smtp_user(self, request: SmtpUserGetRequest) -> APIResponse: + """Get a single SMTP user. + + Args: + request: The get SMTP user request + + Returns: + APIResponse: API response with SMTP user data + """ + response = await self.client.request( + method="GET", + path=f"domains/{request.domain_id}/smtp-users/{request.smtp_user_id}", + ) + return self._create_response(response) + + async def create_smtp_user(self, request: SmtpUserCreateRequest) -> APIResponse: + """Create an SMTP user. + + Args: + request: The create SMTP user request + + Returns: + APIResponse: API response with SMTP user creation data + """ + response = await self.client.request( + method="POST", + path=f"domains/{request.domain_id}/smtp-users", + body=request.to_json(), + ) + return self._create_response(response) + + async def update_smtp_user(self, request: SmtpUserUpdateRequest) -> APIResponse: + """Update an SMTP user. + + Args: + request: The update SMTP user request + + Returns: + APIResponse: API response with updated SMTP user data + """ + response = await self.client.request( + method="PUT", + path=f"domains/{request.domain_id}/smtp-users/{request.smtp_user_id}", + body=request.to_json(), + ) + return self._create_response(response) + + async def delete_smtp_user(self, request: SmtpUserDeleteRequest) -> APIResponse: + """Delete an SMTP user. + + Args: + request: The delete SMTP user request + + Returns: + APIResponse: API response with delete confirmation + """ + response = await self.client.request( + method="DELETE", + path=f"domains/{request.domain_id}/smtp-users/{request.smtp_user_id}", + ) + return self._create_response(response) diff --git a/mailersend/resources/templates.py b/mailersend/resources/templates.py index f26123d..a3c9f41 100644 --- a/mailersend/resources/templates.py +++ b/mailersend/resources/templates.py @@ -2,7 +2,7 @@ from typing import Optional -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.base import APIResponse from ..models.templates import ( TemplatesListRequest, @@ -90,3 +90,57 @@ def delete_template(self, request: TemplateDeleteRequest) -> APIResponse: # Create standardized response return self._create_response(response) + + +class AsyncTemplates(AsyncBaseResource): + """Async client for interacting with the MailerSend Templates API.""" + + async def list_templates( + self, request: Optional[TemplatesListRequest] = None + ) -> APIResponse: + """ + Retrieve a list of templates. + + Args: + request: Optional TemplatesListRequest with filtering and pagination options + + Returns: + APIResponse with TemplatesListResponse data + """ + if request is None: + request = TemplatesListRequest() + params = request.to_query_params() + response = await self.client.request( + method="GET", path="templates", params=params + ) + return self._create_response(response) + + async def get_template(self, request: TemplateGetRequest) -> APIResponse: + """ + Retrieve information about a single template. + + Args: + request: TemplateGetRequest with template ID + + Returns: + APIResponse with TemplateResponse data + """ + response = await self.client.request( + method="GET", path=f"templates/{request.template_id}" + ) + return self._create_response(response) + + async def delete_template(self, request: TemplateDeleteRequest) -> APIResponse: + """ + Delete a template. + + Args: + request: TemplateDeleteRequest with template ID to delete + + Returns: + APIResponse with empty data + """ + response = await self.client.request( + method="DELETE", path=f"templates/{request.template_id}" + ) + return self._create_response(response) diff --git a/mailersend/resources/tokens.py b/mailersend/resources/tokens.py index 08d07ff..b2bf362 100644 --- a/mailersend/resources/tokens.py +++ b/mailersend/resources/tokens.py @@ -1,6 +1,6 @@ """Tokens API resource.""" -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.base import APIResponse from ..models.tokens import ( TokensListRequest, @@ -135,3 +135,92 @@ def delete_token(self, request: TokenDeleteRequest) -> APIResponse: # Create standardized response return self._create_response(response) + + +class AsyncTokens(AsyncBaseResource): + """Async Tokens API resource.""" + + async def list_tokens(self, request: TokensListRequest) -> APIResponse: + """List API tokens. + + Args: + request: The list tokens request + + Returns: + APIResponse: API response with tokens list data + """ + params = request.to_query_params() + response = await self.client.request(method="GET", path="token", params=params) + return self._create_response(response) + + async def get_token(self, request: TokenGetRequest) -> APIResponse: + """Get a single API token. + + Args: + request: The get token request + + Returns: + APIResponse: API response with token data + """ + response = await self.client.request( + method="GET", path=f"token/{request.token_id}" + ) + return self._create_response(response) + + async def create_token(self, request: TokenCreateRequest) -> APIResponse: + """Create an API token. + + Args: + request: The create token request + + Returns: + APIResponse: API response with token creation data + """ + response = await self.client.request( + method="POST", path="token", body=request.to_json() + ) + return self._create_response(response) + + async def update_token(self, request: TokenUpdateRequest) -> APIResponse: + """Update an API token status. + + Args: + request: The update token request + + Returns: + APIResponse: API response with update confirmation + """ + response = await self.client.request( + method="PUT", + path=f"token/{request.token_id}/settings", + body=request.to_json(), + ) + return self._create_response(response) + + async def update_token_name(self, request: TokenUpdateNameRequest) -> APIResponse: + """Update an API token name. + + Args: + request: The update token name request + + Returns: + APIResponse: API response with update confirmation + """ + response = await self.client.request( + method="PUT", path=f"token/{request.token_id}", body=request.to_json() + ) + return self._create_response(response) + + async def delete_token(self, request: TokenDeleteRequest) -> APIResponse: + """Delete an API token. + + Args: + request: The delete token request + + Returns: + APIResponse: API response with delete confirmation + """ + response = await self.client.request( + method="DELETE", path=f"token/{request.token_id}" + ) + return self._create_response(response) diff --git a/mailersend/resources/users.py b/mailersend/resources/users.py index c58fc31..4d34c3c 100644 --- a/mailersend/resources/users.py +++ b/mailersend/resources/users.py @@ -1,6 +1,6 @@ """Users API resource.""" -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.base import APIResponse from ..models.users import ( UsersListRequest, @@ -198,3 +198,133 @@ def cancel_invite(self, request: InviteCancelRequest) -> APIResponse: # Create standardized response return self._create_response(response, None) + + +class AsyncUsers(AsyncBaseResource): + """Async Users API resource.""" + + async def list_users(self, request: UsersListRequest) -> APIResponse: + """Get a list of account users. + + Args: + request: The list users request + + Returns: + APIResponse: API response with users list data + """ + params = request.to_query_params() + response = await self.client.request(method="GET", path="users", params=params) + return self._create_response(response) + + async def get_user(self, request: UserGetRequest) -> APIResponse: + """Get a single account user. + + Args: + request: The get user request + + Returns: + APIResponse: API response with user data + """ + response = await self.client.request( + method="GET", path=f"users/{request.user_id}" + ) + return self._create_response(response) + + async def invite_user(self, request: UserInviteRequest) -> APIResponse: + """Invite a user to account. + + Args: + request: The user invite request + + Returns: + APIResponse: API response with invite data + """ + response = await self.client.request( + method="POST", path="users", body=request.to_json() + ) + return self._create_response(response) + + async def update_user(self, request: UserUpdateRequest) -> APIResponse: + """Update account user. + + Args: + request: The user update request + + Returns: + APIResponse: API response with updated user data + """ + response = await self.client.request( + method="PUT", path=f"users/{request.user_id}", body=request.to_json() + ) + return self._create_response(response) + + async def delete_user(self, request: UserDeleteRequest) -> APIResponse: + """Delete account user. + + Args: + request: The user delete request + + Returns: + APIResponse: API response with delete confirmation + """ + response = await self.client.request( + method="DELETE", path=f"users/{request.user_id}" + ) + return self._create_response(response, None) + + async def list_invites(self, request: InvitesListRequest) -> APIResponse: + """Get a list of invites. + + Args: + request: The list invites request + + Returns: + APIResponse: API response with invites list data + """ + params = request.to_query_params() + response = await self.client.request( + method="GET", path="invites", params=params + ) + return self._create_response(response) + + async def get_invite(self, request: InviteGetRequest) -> APIResponse: + """Get a single invite. + + Args: + request: The get invite request + + Returns: + APIResponse: API response with invite data + """ + response = await self.client.request( + method="GET", path=f"invites/{request.invite_id}" + ) + return self._create_response(response) + + async def resend_invite(self, request: InviteResendRequest) -> APIResponse: + """Resend an invite. + + Args: + request: The invite resend request + + Returns: + APIResponse: API response with resent invite data + """ + response = await self.client.request( + method="POST", path=f"invites/{request.invite_id}/resend" + ) + return self._create_response(response) + + async def cancel_invite(self, request: InviteCancelRequest) -> APIResponse: + """Cancel an invite. + + Args: + request: The invite cancel request + + Returns: + APIResponse: API response with cancel confirmation + """ + response = await self.client.request( + method="DELETE", path=f"invites/{request.invite_id}" + ) + return self._create_response(response, None) diff --git a/mailersend/resources/webhooks.py b/mailersend/resources/webhooks.py index 031c1ff..418c2ea 100644 --- a/mailersend/resources/webhooks.py +++ b/mailersend/resources/webhooks.py @@ -1,6 +1,6 @@ """Webhooks resource for MailerSend SDK.""" -from .base import BaseResource +from .base import AsyncBaseResource, BaseResource from ..models.base import APIResponse from ..models.webhooks import ( WebhooksListRequest, @@ -123,3 +123,78 @@ def delete_webhook(self, request: WebhookDeleteRequest) -> APIResponse: # Create standardized response return self._create_response(response) + + +class AsyncWebhooks(AsyncBaseResource): + """Async Webhooks API resource.""" + + async def list_webhooks(self, request: WebhooksListRequest) -> APIResponse: + """List webhooks for a domain. + + Args: + request: The webhooks list request + + Returns: + APIResponse with WebhooksListResponse data + """ + params = request.to_query_params() + response = await self.client.request( + method="GET", path="webhooks", params=params + ) + return self._create_response(response) + + async def get_webhook(self, request: WebhookGetRequest) -> APIResponse: + """Get a single webhook by ID. + + Args: + request: The webhook get request + + Returns: + APIResponse with WebhookResponse data + """ + response = await self.client.request( + method="GET", path=f"webhooks/{request.webhook_id}" + ) + return self._create_response(response) + + async def create_webhook(self, request: WebhookCreateRequest) -> APIResponse: + """Create a new webhook. + + Args: + request: The webhook create request + + Returns: + APIResponse with WebhookResponse data + """ + data = request.model_dump(exclude_none=True) + response = await self.client.request(method="POST", path="webhooks", body=data) + return self._create_response(response) + + async def update_webhook(self, request: WebhookUpdateRequest) -> APIResponse: + """Update an existing webhook. + + Args: + request: The webhook update request + + Returns: + APIResponse with WebhookResponse data + """ + data = request.model_dump(exclude_none=True, exclude={"webhook_id"}) + response = await self.client.request( + method="PUT", path=f"webhooks/{request.webhook_id}", body=data + ) + return self._create_response(response) + + async def delete_webhook(self, request: WebhookDeleteRequest) -> APIResponse: + """Delete a webhook. + + Args: + request: The webhook delete request + + Returns: + APIResponse with empty data + """ + response = await self.client.request( + method="DELETE", path=f"webhooks/{request.webhook_id}" + ) + return self._create_response(response) diff --git a/poetry.lock b/poetry.lock index 28b05fd..4f6613d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -12,6 +12,39 @@ files = [ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] +[[package]] +name = "anyio" +version = "4.13.0" +description = "High-level concurrency and networking framework on top of asyncio or Trio" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708"}, + {file = "anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +trio = ["trio (>=0.32.0)"] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." +optional = false +python-versions = "<3.11,>=3.8" +groups = ["dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, + {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, +] + [[package]] name = "backports-tarfile" version = "1.2.0" @@ -646,7 +679,7 @@ version = "1.3.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" -groups = ["dev"] +groups = ["main", "dev"] markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, @@ -705,6 +738,65 @@ gitdb = ">=4.0.1,<5" doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy (==1.18.2) ; python_version >= \"3.9\"", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "identify" version = "2.6.15" @@ -1273,6 +1365,27 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5"}, + {file = "pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5"}, +] + +[package.dependencies] +backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} +pytest = ">=8.2,<10" +typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + [[package]] name = "pytest-mock" version = "3.15.1" @@ -1744,7 +1857,7 @@ files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] -markers = {dev = "python_version == \"3.10\""} +markers = {dev = "python_version < \"3.13\""} [[package]] name = "typing-inspection" @@ -1978,4 +2091,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "45c6d161702f919e8ca9cdcf32dc4bdba0bfe0581079a17f01db0e96c4bde21b" +content-hash = "6b4448a03cd02dc5c953328a65283eeb8c408ad2ea4691d356733c3ef0ef12a2" diff --git a/pyproject.toml b/pyproject.toml index 5b6dc5e..89f17ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,17 +8,23 @@ version = "2.0.3" python = "^3.10" requests = "^2.28.1" pydantic = {extras = ["email"], version = "^2.11.0"} +httpx = "^0.28.1" [tool.poetry.dev-dependencies] black = "^26.0.0" coverage = "^7.0.0" pytest = "^9.0.0" +pytest-asyncio = "^1.3.0" pytest-mock = "^3.10.0" python-dotenv = "^0.21.0" python-semantic-release = "^7.32.2" vcrpy = "^8.0.0" pre-commit = "^2.12.1" +[tool.pytest.ini_options] +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" + [build-system] build-backend = "poetry.core.masonry.api" requires = ["poetry-core>=1.0.0"] diff --git a/tests/unit/test_async_activity_resource.py b/tests/unit/test_async_activity_resource.py new file mode 100644 index 0000000..0ddc8bc --- /dev/null +++ b/tests/unit/test_async_activity_resource.py @@ -0,0 +1,67 @@ +"""Tests for AsyncActivity resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.activity import AsyncActivity +from mailersend.models.activity import ( + ActivityRequest, + ActivityQueryParams, + SingleActivityRequest, +) +from mailersend.models.base import APIResponse + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncActivity: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncActivity(self.mock_client) + + async def test_get_returns_api_response(self): + request = ActivityRequest( + domain_id="domain123", + query_params=ActivityQueryParams(date_from=1443651141, date_to=1443661141), + ) + result = await self.resource.get(request) + assert isinstance(result, APIResponse) + + async def test_get_calls_correct_endpoint(self): + request = ActivityRequest( + domain_id="domain123", + query_params=ActivityQueryParams(date_from=1443651141, date_to=1443661141), + ) + await self.resource.get(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert "activity/domain123" in call.kwargs["path"] + + async def test_get_passes_query_params(self): + request = ActivityRequest( + domain_id="domain123", + query_params=ActivityQueryParams(date_from=1443651141, date_to=1443661141), + ) + await self.resource.get(request) + call = self.mock_client.request.call_args + assert "params" in call.kwargs + assert call.kwargs["params"] is not None + + async def test_get_single_returns_api_response(self): + request = SingleActivityRequest(activity_id="activity123") + result = await self.resource.get_single(request) + assert isinstance(result, APIResponse) + + async def test_get_single_calls_correct_endpoint(self): + request = SingleActivityRequest(activity_id="activity123") + await self.resource.get_single(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "activities/activity123" diff --git a/tests/unit/test_async_analytics_resource.py b/tests/unit/test_async_analytics_resource.py new file mode 100644 index 0000000..cff82ab --- /dev/null +++ b/tests/unit/test_async_analytics_resource.py @@ -0,0 +1,83 @@ +"""Tests for AsyncAnalytics resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.analytics import AsyncAnalytics +from mailersend.models.analytics import AnalyticsRequest +from mailersend.models.base import APIResponse + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +def _make_request(): + return AnalyticsRequest( + date_from=1443651141, + date_to=1443661141, + tags=["newsletter"], + event=["sent", "delivered"], + ) + + +class TestAsyncAnalytics: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncAnalytics(self.mock_client) + + async def test_get_activity_by_date_returns_api_response(self): + result = await self.resource.get_activity_by_date(_make_request()) + assert isinstance(result, APIResponse) + + async def test_get_activity_by_date_calls_correct_endpoint(self): + await self.resource.get_activity_by_date(_make_request()) + call = self.mock_client.request.call_args + assert call.args == ("GET", "analytics/date") + + async def test_get_activity_by_date_passes_params(self): + await self.resource.get_activity_by_date(_make_request()) + call = self.mock_client.request.call_args + params = call.kwargs["params"] + assert "date_from" in params + assert "date_to" in params + + async def test_get_opens_by_country_returns_api_response(self): + result = await self.resource.get_opens_by_country(_make_request()) + assert isinstance(result, APIResponse) + + async def test_get_opens_by_country_calls_correct_endpoint(self): + await self.resource.get_opens_by_country(_make_request()) + call = self.mock_client.request.call_args + assert call.args == ("GET", "analytics/country") + + async def test_get_opens_by_country_excludes_event_and_group_by(self): + await self.resource.get_opens_by_country(_make_request()) + call = self.mock_client.request.call_args + params = call.kwargs["params"] + assert "event" not in params + assert "event[]" not in params + assert "group_by" not in params + + async def test_get_opens_by_user_agent_returns_api_response(self): + result = await self.resource.get_opens_by_user_agent(_make_request()) + assert isinstance(result, APIResponse) + + async def test_get_opens_by_user_agent_calls_correct_endpoint(self): + await self.resource.get_opens_by_user_agent(_make_request()) + call = self.mock_client.request.call_args + assert call.args == ("GET", "analytics/ua-name") + + async def test_get_opens_by_reading_environment_returns_api_response(self): + result = await self.resource.get_opens_by_reading_environment(_make_request()) + assert isinstance(result, APIResponse) + + async def test_get_opens_by_reading_environment_calls_correct_endpoint(self): + await self.resource.get_opens_by_reading_environment(_make_request()) + call = self.mock_client.request.call_args + assert call.args == ("GET", "analytics/ua-type") diff --git a/tests/unit/test_async_client.py b/tests/unit/test_async_client.py new file mode 100644 index 0000000..fa454d7 --- /dev/null +++ b/tests/unit/test_async_client.py @@ -0,0 +1,311 @@ +"""Tests for AsyncMailerSendClient initialization and behaviour.""" + +import os +import pytest +import httpx +from unittest.mock import AsyncMock, MagicMock, patch + +from mailersend.async_client import AsyncMailerSendClient +from mailersend.exceptions import ( + AuthenticationError, + BadRequestError, + MailerSendError, + RateLimitExceeded, + ResourceNotFoundError, + ServerError, +) + + +class TestAsyncMailerSendClientInit: + def test_init_with_explicit_api_key(self): + with patch("mailersend.async_client.httpx.AsyncClient"): + client = AsyncMailerSendClient(api_key="test-key") + assert client.api_key == "test-key" + + def test_init_reads_env_var(self): + with patch.dict(os.environ, {"MAILERSEND_API_KEY": "env-key"}), patch( + "mailersend.async_client.httpx.AsyncClient" + ): + client = AsyncMailerSendClient() + assert client.api_key == "env-key" + + def test_init_explicit_key_overrides_env(self): + with patch.dict(os.environ, {"MAILERSEND_API_KEY": "env-key"}), patch( + "mailersend.async_client.httpx.AsyncClient" + ): + client = AsyncMailerSendClient(api_key="param-key") + assert client.api_key == "param-key" + + def test_init_raises_without_api_key(self): + with patch.dict(os.environ, {}, clear=True), patch( + "mailersend.async_client.httpx.AsyncClient" + ): + with pytest.raises(ValueError, match="API key is required"): + AsyncMailerSendClient() + + def test_init_sets_all_properties(self): + with patch("mailersend.async_client.httpx.AsyncClient"): + client = AsyncMailerSendClient( + api_key="test-key", + base_url="https://custom.api.com/v1/", + timeout=60, + max_retries=5, + debug=True, + ) + assert client.base_url == "https://custom.api.com/v1/" + assert client.timeout == 60 + assert client.max_retries == 5 + assert client.debug is True + + def test_init_exposes_all_resources(self): + with patch("mailersend.async_client.httpx.AsyncClient"): + client = AsyncMailerSendClient(api_key="test-key") + assert hasattr(client, "emails") + assert hasattr(client, "domains") + assert hasattr(client, "activities") + assert hasattr(client, "analytics") + assert hasattr(client, "webhooks") + assert hasattr(client, "templates") + assert hasattr(client, "recipients") + assert hasattr(client, "sms_sending") + assert hasattr(client, "dmarc_monitoring") + + +class TestAsyncMailerSendClientRequest: + def _make_mock_response(self, status_code=200, json_data=None, headers=None): + response = MagicMock() + response.status_code = status_code + response.headers = headers or {"x-request-id": "req-123"} + response.json.return_value = json_data or {} + response.text = "" + response.content = b"{}" + return response + + async def test_successful_get_request(self): + mock_response = self._make_mock_response(200, {"data": "value"}) + + with patch("mailersend.async_client.httpx.AsyncClient") as MockClient: + mock_http = AsyncMock() + mock_http.request = AsyncMock(return_value=mock_response) + MockClient.return_value = mock_http + + client = AsyncMailerSendClient(api_key="test-key") + client._client = mock_http + + result = await client.request("GET", "some-endpoint") + assert result.status_code == 200 + + async def test_raises_authentication_error_on_401(self): + mock_response = self._make_mock_response(401, {"message": "Unauthorized"}) + + with patch("mailersend.async_client.httpx.AsyncClient"): + client = AsyncMailerSendClient(api_key="test-key") + client._client = AsyncMock() + client._client.request = AsyncMock(return_value=mock_response) + + with pytest.raises(AuthenticationError): + await client.request("GET", "some-endpoint") + + async def test_raises_not_found_on_404(self): + mock_response = self._make_mock_response(404, {"message": "Not found"}) + + with patch("mailersend.async_client.httpx.AsyncClient"): + client = AsyncMailerSendClient(api_key="test-key") + client._client = AsyncMock() + client._client.request = AsyncMock(return_value=mock_response) + + with pytest.raises(ResourceNotFoundError): + await client.request("GET", "some-endpoint") + + async def test_raises_rate_limit_on_429(self): + mock_response = self._make_mock_response(429, {"message": "Too many requests"}) + mock_response.headers = {"retry-after": "60", "x-apiquota-remaining": "0"} + + with patch("mailersend.async_client.httpx.AsyncClient"): + client = AsyncMailerSendClient(api_key="test-key", max_retries=0) + client._client = AsyncMock() + client._client.request = AsyncMock(return_value=mock_response) + + with pytest.raises(RateLimitExceeded): + await client.request("GET", "some-endpoint") + + async def test_raises_bad_request_on_400(self): + mock_response = self._make_mock_response(400, {"message": "Bad request"}) + + with patch("mailersend.async_client.httpx.AsyncClient"): + client = AsyncMailerSendClient(api_key="test-key") + client._client = AsyncMock() + client._client.request = AsyncMock(return_value=mock_response) + + with pytest.raises(BadRequestError): + await client.request("GET", "some-endpoint") + + async def test_raises_server_error_on_500(self): + mock_response = self._make_mock_response(500, {"message": "Server error"}) + + with patch("mailersend.async_client.httpx.AsyncClient"): + client = AsyncMailerSendClient(api_key="test-key", max_retries=0) + client._client = AsyncMock() + client._client.request = AsyncMock(return_value=mock_response) + + with pytest.raises(ServerError): + await client.request("GET", "some-endpoint") + + async def test_retries_on_500_then_succeeds(self): + error_response = self._make_mock_response(500, {"message": "Server error"}) + ok_response = self._make_mock_response(200, {"data": "ok"}) + + with patch("mailersend.async_client.httpx.AsyncClient"), patch( + "mailersend.async_client.asyncio.sleep", new_callable=AsyncMock + ): + client = AsyncMailerSendClient(api_key="test-key", max_retries=2) + client._client = AsyncMock() + client._client.request = AsyncMock( + side_effect=[error_response, ok_response] + ) + + result = await client.request("GET", "some-endpoint") + assert result.status_code == 200 + assert client._client.request.call_count == 2 + + async def test_exhausting_retries_raises_server_error(self): + error_response = self._make_mock_response(500, {"message": "Server error"}) + + with patch("mailersend.async_client.httpx.AsyncClient"), patch( + "mailersend.async_client.asyncio.sleep", new_callable=AsyncMock + ): + client = AsyncMailerSendClient(api_key="test-key", max_retries=2) + client._client = AsyncMock() + client._client.request = AsyncMock(return_value=error_response) + + with pytest.raises(ServerError): + await client.request("GET", "some-endpoint") + + assert client._client.request.call_count == 3 # initial + 2 retries + + async def test_retry_uses_backoff_delay(self): + error_response = self._make_mock_response(500, {"message": "Server error"}) + ok_response = self._make_mock_response(200, {}) + + with patch("mailersend.async_client.httpx.AsyncClient"), patch( + "mailersend.async_client.asyncio.sleep", new_callable=AsyncMock + ) as mock_sleep: + client = AsyncMailerSendClient(api_key="test-key", max_retries=2) + client._client = AsyncMock() + client._client.request = AsyncMock( + side_effect=[error_response, error_response, ok_response] + ) + + await client.request("GET", "some-endpoint") + + assert mock_sleep.call_count == 2 + assert mock_sleep.call_args_list[0].args[0] == pytest.approx( + 0.3 + ) # 0.3 * 2^0 + assert mock_sleep.call_args_list[1].args[0] == pytest.approx( + 0.6 + ) # 0.3 * 2^1 + + async def test_429_retry_uses_retry_after_header(self): + rate_limit_response = self._make_mock_response( + 429, {"message": "Too many requests"} + ) + rate_limit_response.headers = {"retry-after": "30", "x-apiquota-remaining": "0"} + ok_response = self._make_mock_response(200, {}) + + with patch("mailersend.async_client.httpx.AsyncClient"), patch( + "mailersend.async_client.asyncio.sleep", new_callable=AsyncMock + ) as mock_sleep: + client = AsyncMailerSendClient(api_key="test-key", max_retries=1) + client._client = AsyncMock() + client._client.request = AsyncMock( + side_effect=[rate_limit_response, ok_response] + ) + + await client.request("GET", "some-endpoint") + + mock_sleep.assert_called_once_with(30.0) + + async def test_429_retry_falls_back_to_backoff_without_retry_after(self): + rate_limit_response = self._make_mock_response( + 429, {"message": "Too many requests"} + ) + rate_limit_response.headers = {} + ok_response = self._make_mock_response(200, {}) + + with patch("mailersend.async_client.httpx.AsyncClient"), patch( + "mailersend.async_client.asyncio.sleep", new_callable=AsyncMock + ) as mock_sleep: + client = AsyncMailerSendClient(api_key="test-key", max_retries=1) + client._client = AsyncMock() + client._client.request = AsyncMock( + side_effect=[rate_limit_response, ok_response] + ) + + await client.request("GET", "some-endpoint") + + mock_sleep.assert_called_once_with(pytest.approx(0.3)) + + async def test_raises_mailer_send_error_on_unexpected_status(self): + mock_response = self._make_mock_response(418, {"message": "I'm a teapot"}) + + with patch("mailersend.async_client.httpx.AsyncClient"): + client = AsyncMailerSendClient(api_key="test-key") + client._client = AsyncMock() + client._client.request = AsyncMock(return_value=mock_response) + + with pytest.raises(MailerSendError): + await client.request("GET", "some-endpoint") + + async def test_retries_on_network_error_then_succeeds(self): + ok_response = self._make_mock_response(200, {"data": "ok"}) + + with patch("mailersend.async_client.httpx.AsyncClient"), patch( + "mailersend.async_client.asyncio.sleep", new_callable=AsyncMock + ): + client = AsyncMailerSendClient(api_key="test-key", max_retries=2) + client._client = AsyncMock() + client._client.request = AsyncMock( + side_effect=[httpx.ConnectError("Connection refused"), ok_response] + ) + + result = await client.request("GET", "some-endpoint") + assert result.status_code == 200 + assert client._client.request.call_count == 2 + + async def test_exhausting_retries_on_network_error_raises(self): + with patch("mailersend.async_client.httpx.AsyncClient"), patch( + "mailersend.async_client.asyncio.sleep", new_callable=AsyncMock + ): + client = AsyncMailerSendClient(api_key="test-key", max_retries=2) + client._client = AsyncMock() + client._client.request = AsyncMock( + side_effect=httpx.ConnectError("Connection refused") + ) + + with pytest.raises(MailerSendError, match="Request failed"): + await client.request("GET", "some-endpoint") + + assert client._client.request.call_count == 3 + + +class TestAsyncMailerSendClientContextManager: + async def test_context_manager_calls_close(self): + with patch("mailersend.async_client.httpx.AsyncClient"): + client = AsyncMailerSendClient(api_key="test-key") + client._client = AsyncMock() + client._client.aclose = AsyncMock() + + async with client as c: + assert c is client + + client._client.aclose.assert_called_once() + + async def test_close_method(self): + with patch("mailersend.async_client.httpx.AsyncClient"): + client = AsyncMailerSendClient(api_key="test-key") + client._client = AsyncMock() + client._client.aclose = AsyncMock() + + await client.close() + client._client.aclose.assert_called_once() diff --git a/tests/unit/test_async_dmarc_monitoring_resource.py b/tests/unit/test_async_dmarc_monitoring_resource.py new file mode 100644 index 0000000..84cfb40 --- /dev/null +++ b/tests/unit/test_async_dmarc_monitoring_resource.py @@ -0,0 +1,153 @@ +"""Tests for AsyncDmarcMonitoring resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.dmarc_monitoring import AsyncDmarcMonitoring +from mailersend.models.base import APIResponse +from mailersend.models.dmarc_monitoring import ( + DmarcMonitoringListRequest, + DmarcMonitoringListQueryParams, + DmarcMonitoringCreateRequest, + DmarcMonitoringUpdateRequest, + DmarcMonitoringDeleteRequest, + DmarcMonitoringReportRequest, + DmarcMonitoringReportQueryParams, + DmarcMonitoringIpReportRequest, + DmarcMonitoringReportSourcesRequest, + DmarcMonitoringFavoriteRequest, +) + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncDmarcMonitoring: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncDmarcMonitoring(self.mock_client) + + async def test_list_monitors_returns_api_response(self): + result = await self.resource.list_monitors() + assert isinstance(result, APIResponse) + + async def test_list_monitors_calls_correct_endpoint(self): + await self.resource.list_monitors() + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "dmarc-monitoring" + + async def test_list_monitors_with_request(self): + request = DmarcMonitoringListRequest( + query_params=DmarcMonitoringListQueryParams(page=2, limit=10) + ) + await self.resource.list_monitors(request) + call = self.mock_client.request.call_args + assert call.kwargs["params"]["page"] == 2 + + async def test_create_monitor_returns_api_response(self): + request = DmarcMonitoringCreateRequest(domain_id="dom123") + result = await self.resource.create_monitor(request) + assert isinstance(result, APIResponse) + + async def test_create_monitor_calls_correct_endpoint(self): + request = DmarcMonitoringCreateRequest(domain_id="dom123") + await self.resource.create_monitor(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "POST" + assert call.kwargs["path"] == "dmarc-monitoring" + + async def test_update_monitor_returns_api_response(self): + request = DmarcMonitoringUpdateRequest( + monitor_id="mon123", wanted_dmarc_record="v=DMARC1; p=none;" + ) + result = await self.resource.update_monitor(request) + assert isinstance(result, APIResponse) + + async def test_update_monitor_calls_correct_endpoint(self): + request = DmarcMonitoringUpdateRequest( + monitor_id="mon123", wanted_dmarc_record="v=DMARC1; p=none;" + ) + await self.resource.update_monitor(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "PUT" + assert call.kwargs["path"] == "dmarc-monitoring/mon123" + + async def test_delete_monitor_returns_api_response(self): + request = DmarcMonitoringDeleteRequest(monitor_id="mon123") + result = await self.resource.delete_monitor(request) + assert isinstance(result, APIResponse) + + async def test_delete_monitor_calls_correct_endpoint(self): + await self.resource.delete_monitor( + DmarcMonitoringDeleteRequest(monitor_id="mon123") + ) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "dmarc-monitoring/mon123" + + async def test_get_aggregated_report_returns_api_response(self): + request = DmarcMonitoringReportRequest(monitor_id="mon123") + result = await self.resource.get_aggregated_report(request) + assert isinstance(result, APIResponse) + + async def test_get_aggregated_report_calls_correct_endpoint(self): + request = DmarcMonitoringReportRequest(monitor_id="mon123") + await self.resource.get_aggregated_report(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "dmarc-monitoring/mon123/report" + + async def test_get_ip_report_returns_api_response(self): + request = DmarcMonitoringIpReportRequest(monitor_id="mon123", ip="1.2.3.4") + result = await self.resource.get_ip_report(request) + assert isinstance(result, APIResponse) + + async def test_get_ip_report_calls_correct_endpoint(self): + request = DmarcMonitoringIpReportRequest(monitor_id="mon123", ip="1.2.3.4") + await self.resource.get_ip_report(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "dmarc-monitoring/mon123/report/1.2.3.4" + + async def test_get_report_sources_returns_api_response(self): + request = DmarcMonitoringReportSourcesRequest(monitor_id="mon123") + result = await self.resource.get_report_sources(request) + assert isinstance(result, APIResponse) + + async def test_get_report_sources_calls_correct_endpoint(self): + request = DmarcMonitoringReportSourcesRequest(monitor_id="mon123") + await self.resource.get_report_sources(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "dmarc-monitoring/mon123/report-sources" + + async def test_mark_ip_favorite_returns_api_response(self): + request = DmarcMonitoringFavoriteRequest(monitor_id="mon123", ip="1.2.3.4") + result = await self.resource.mark_ip_favorite(request) + assert isinstance(result, APIResponse) + + async def test_mark_ip_favorite_calls_correct_endpoint(self): + request = DmarcMonitoringFavoriteRequest(monitor_id="mon123", ip="1.2.3.4") + await self.resource.mark_ip_favorite(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "PUT" + assert call.kwargs["path"] == "dmarc-monitoring/mon123/favorite/1.2.3.4" + + async def test_remove_ip_favorite_returns_api_response(self): + request = DmarcMonitoringFavoriteRequest(monitor_id="mon123", ip="1.2.3.4") + result = await self.resource.remove_ip_favorite(request) + assert isinstance(result, APIResponse) + + async def test_remove_ip_favorite_calls_correct_endpoint(self): + request = DmarcMonitoringFavoriteRequest(monitor_id="mon123", ip="1.2.3.4") + await self.resource.remove_ip_favorite(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "dmarc-monitoring/mon123/favorite/1.2.3.4" diff --git a/tests/unit/test_async_domains_resource.py b/tests/unit/test_async_domains_resource.py new file mode 100644 index 0000000..0313fd3 --- /dev/null +++ b/tests/unit/test_async_domains_resource.py @@ -0,0 +1,148 @@ +"""Tests for AsyncDomains resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.domains import AsyncDomains +from mailersend.models.domains import ( + DomainListRequest, + DomainListQueryParams, + DomainCreateRequest, + DomainGetRequest, + DomainDeleteRequest, + DomainUpdateSettingsRequest, + DomainRecipientsRequest, + DomainRecipientsQueryParams, + DomainDnsRecordsRequest, + DomainVerificationRequest, +) +from mailersend.models.base import APIResponse + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncDomains: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncDomains(self.mock_client) + + async def test_list_domains_returns_api_response(self): + result = await self.resource.list_domains() + assert isinstance(result, APIResponse) + + async def test_list_domains_uses_default_params(self): + await self.resource.list_domains() + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "domains" + assert call.kwargs["params"] == {"page": 1, "limit": 25} + + async def test_list_domains_with_custom_params(self): + request = DomainListRequest( + query_params=DomainListQueryParams(page=2, limit=50, verified=True) + ) + await self.resource.list_domains(request) + call = self.mock_client.request.call_args + assert call.kwargs["params"]["page"] == 2 + assert call.kwargs["params"]["limit"] == 50 + + async def test_get_domain_returns_api_response(self): + result = await self.resource.get_domain(DomainGetRequest(domain_id="dom123")) + assert isinstance(result, APIResponse) + + async def test_get_domain_calls_correct_endpoint(self): + await self.resource.get_domain(DomainGetRequest(domain_id="dom123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "domains/dom123" + + async def test_create_domain_returns_api_response(self): + result = await self.resource.create_domain( + DomainCreateRequest(name="example.com") + ) + assert isinstance(result, APIResponse) + + async def test_create_domain_calls_correct_endpoint(self): + await self.resource.create_domain(DomainCreateRequest(name="example.com")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "POST" + assert call.kwargs["path"] == "domains" + assert call.kwargs["body"]["name"] == "example.com" + + async def test_delete_domain_returns_api_response(self): + result = await self.resource.delete_domain( + DomainDeleteRequest(domain_id="dom123") + ) + assert isinstance(result, APIResponse) + + async def test_delete_domain_calls_correct_endpoint(self): + await self.resource.delete_domain(DomainDeleteRequest(domain_id="dom123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "domains/dom123" + + async def test_get_domain_recipients_returns_api_response(self): + request = DomainRecipientsRequest( + domain_id="dom123", + query_params=DomainRecipientsQueryParams(), + ) + result = await self.resource.get_domain_recipients(request) + assert isinstance(result, APIResponse) + + async def test_get_domain_recipients_calls_correct_endpoint(self): + request = DomainRecipientsRequest( + domain_id="dom123", query_params=DomainRecipientsQueryParams() + ) + await self.resource.get_domain_recipients(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "domains/dom123/recipients" + + async def test_update_domain_settings_returns_api_response(self): + request = DomainUpdateSettingsRequest(domain_id="dom123", track_opens=True) + result = await self.resource.update_domain_settings(request) + assert isinstance(result, APIResponse) + + async def test_update_domain_settings_excludes_domain_id_from_body(self): + request = DomainUpdateSettingsRequest(domain_id="dom123", track_opens=True) + await self.resource.update_domain_settings(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "PUT" + assert call.kwargs["path"] == "domains/dom123/settings" + assert "domain_id" not in call.kwargs["body"] + assert call.kwargs["body"]["track_opens"] is True + + async def test_get_domain_dns_records_returns_api_response(self): + result = await self.resource.get_domain_dns_records( + DomainDnsRecordsRequest(domain_id="dom123") + ) + assert isinstance(result, APIResponse) + + async def test_get_domain_dns_records_calls_correct_endpoint(self): + await self.resource.get_domain_dns_records( + DomainDnsRecordsRequest(domain_id="dom123") + ) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "domains/dom123/dns-records" + + async def test_get_domain_verification_status_returns_api_response(self): + result = await self.resource.get_domain_verification_status( + DomainVerificationRequest(domain_id="dom123") + ) + assert isinstance(result, APIResponse) + + async def test_get_domain_verification_status_calls_correct_endpoint(self): + await self.resource.get_domain_verification_status( + DomainVerificationRequest(domain_id="dom123") + ) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "domains/dom123/verify" diff --git a/tests/unit/test_async_email_resource.py b/tests/unit/test_async_email_resource.py new file mode 100644 index 0000000..73e199c --- /dev/null +++ b/tests/unit/test_async_email_resource.py @@ -0,0 +1,89 @@ +"""Tests for AsyncEmail resource.""" + +import pytest +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.email import AsyncEmail +from mailersend.models.base import APIResponse +from mailersend.models.email import EmailRequest, EmailContact + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock() + return client + + +def _make_mock_response(status_code=200, json_data=None, headers=None): + response = MagicMock() + response.status_code = status_code + response.headers = headers or {"x-request-id": "req-123", "x-message-id": "msg-456"} + response.json.return_value = json_data or {} + response.content = b"{}" + return response + + +def _make_email_request(): + return EmailRequest( + from_email=EmailContact(email="sender@example.com", name="Sender"), + to=[EmailContact(email="recipient@example.com", name="Recipient")], + subject="Test Subject", + text="Test body", + ) + + +class TestAsyncEmail: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncEmail(self.mock_client) + + async def test_send_returns_api_response(self): + self.mock_client.request.return_value = _make_mock_response() + email = _make_email_request() + + result = await self.resource.send(email) + + assert isinstance(result, APIResponse) + + async def test_send_calls_correct_endpoint(self): + self.mock_client.request.return_value = _make_mock_response() + email = _make_email_request() + + await self.resource.send(email) + + self.mock_client.request.assert_called_once() + call_kwargs = self.mock_client.request.call_args + assert call_kwargs.kwargs["method"] == "POST" + assert call_kwargs.kwargs["path"] == "email" + + async def test_send_includes_message_id_in_response(self): + mock_response = _make_mock_response( + headers={"x-request-id": "req-123", "x-message-id": "msg-789"} + ) + self.mock_client.request.return_value = mock_response + email = _make_email_request() + + result = await self.resource.send(email) + + assert result.data.get("id") == "msg-789" + + async def test_send_bulk_calls_correct_endpoint(self): + self.mock_client.request.return_value = _make_mock_response() + emails = [_make_email_request(), _make_email_request()] + + result = await self.resource.send_bulk(emails) + + assert isinstance(result, APIResponse) + call_kwargs = self.mock_client.request.call_args + assert call_kwargs.kwargs["method"] == "POST" + assert call_kwargs.kwargs["path"] == "bulk-email" + + async def test_get_bulk_status_calls_correct_endpoint(self): + self.mock_client.request.return_value = _make_mock_response() + + result = await self.resource.get_bulk_status("bulk-id-123") + + assert isinstance(result, APIResponse) + call_kwargs = self.mock_client.request.call_args + assert call_kwargs.kwargs["method"] == "GET" + assert "bulk-id-123" in call_kwargs.kwargs["path"] diff --git a/tests/unit/test_async_email_verification_resource.py b/tests/unit/test_async_email_verification_resource.py new file mode 100644 index 0000000..491ac52 --- /dev/null +++ b/tests/unit/test_async_email_verification_resource.py @@ -0,0 +1,155 @@ +"""Tests for AsyncEmailVerification resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.email_verification import AsyncEmailVerification +from mailersend.models.base import APIResponse +from mailersend.models.email_verification import ( + EmailVerifyRequest, + EmailVerifyAsyncRequest, + EmailVerificationAsyncStatusRequest, + EmailVerificationListsRequest, + EmailVerificationListsQueryParams, + EmailVerificationGetRequest, + EmailVerificationCreateRequest, + EmailVerificationVerifyRequest, + EmailVerificationResultsRequest, + EmailVerificationResultsQueryParams, +) + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncEmailVerification: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncEmailVerification(self.mock_client) + + async def test_verify_email_returns_api_response(self): + result = await self.resource.verify_email( + EmailVerifyRequest(email="test@example.com") + ) + assert isinstance(result, APIResponse) + + async def test_verify_email_calls_correct_endpoint(self): + await self.resource.verify_email(EmailVerifyRequest(email="test@example.com")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "POST" + assert call.kwargs["path"] == "email-verification/verify" + assert call.kwargs["body"]["email"] == "test@example.com" + + async def test_verify_email_async_returns_api_response(self): + result = await self.resource.verify_email_async( + EmailVerifyAsyncRequest(email="test@example.com") + ) + assert isinstance(result, APIResponse) + + async def test_verify_email_async_calls_correct_endpoint(self): + await self.resource.verify_email_async( + EmailVerifyAsyncRequest(email="test@example.com") + ) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "POST" + assert call.kwargs["path"] == "email-verification/verify-async" + + async def test_get_async_status_returns_api_response(self): + result = await self.resource.get_async_status( + EmailVerificationAsyncStatusRequest(email_verification_id="abc123") + ) + assert isinstance(result, APIResponse) + + async def test_get_async_status_calls_correct_endpoint(self): + await self.resource.get_async_status( + EmailVerificationAsyncStatusRequest(email_verification_id="abc123") + ) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "email-verification/verify-async/abc123" + + async def test_list_verifications_returns_api_response(self): + request = EmailVerificationListsRequest( + query_params=EmailVerificationListsQueryParams() + ) + result = await self.resource.list_verifications(request) + assert isinstance(result, APIResponse) + + async def test_list_verifications_calls_correct_endpoint(self): + request = EmailVerificationListsRequest( + query_params=EmailVerificationListsQueryParams() + ) + await self.resource.list_verifications(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "email-verification" + assert call.kwargs["params"] == {"page": 1, "limit": 25} + + async def test_get_verification_returns_api_response(self): + result = await self.resource.get_verification( + EmailVerificationGetRequest(email_verification_id="abc123") + ) + assert isinstance(result, APIResponse) + + async def test_get_verification_calls_correct_endpoint(self): + await self.resource.get_verification( + EmailVerificationGetRequest(email_verification_id="abc123") + ) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "email-verification/abc123" + + async def test_create_verification_returns_api_response(self): + request = EmailVerificationCreateRequest( + name="Test List", emails=["a@example.com", "b@example.com"] + ) + result = await self.resource.create_verification(request) + assert isinstance(result, APIResponse) + + async def test_create_verification_calls_correct_endpoint(self): + request = EmailVerificationCreateRequest( + name="Test List", emails=["a@example.com"] + ) + await self.resource.create_verification(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "POST" + assert call.kwargs["path"] == "email-verification" + assert call.kwargs["body"]["name"] == "Test List" + + async def test_verify_list_returns_api_response(self): + result = await self.resource.verify_list( + EmailVerificationVerifyRequest(email_verification_id="abc123") + ) + assert isinstance(result, APIResponse) + + async def test_verify_list_calls_correct_endpoint(self): + await self.resource.verify_list( + EmailVerificationVerifyRequest(email_verification_id="abc123") + ) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "email-verification/abc123/verify" + + async def test_get_results_returns_api_response(self): + request = EmailVerificationResultsRequest( + email_verification_id="abc123", + query_params=EmailVerificationResultsQueryParams(), + ) + result = await self.resource.get_results(request) + assert isinstance(result, APIResponse) + + async def test_get_results_calls_correct_endpoint(self): + request = EmailVerificationResultsRequest( + email_verification_id="abc123", + query_params=EmailVerificationResultsQueryParams(), + ) + await self.resource.get_results(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "email-verification/abc123/results" diff --git a/tests/unit/test_async_identities_resource.py b/tests/unit/test_async_identities_resource.py new file mode 100644 index 0000000..875301d --- /dev/null +++ b/tests/unit/test_async_identities_resource.py @@ -0,0 +1,141 @@ +"""Tests for AsyncIdentitiesResource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.identities import AsyncIdentitiesResource +from mailersend.models.base import APIResponse +from mailersend.models.identities import ( + IdentityListRequest, + IdentityListQueryParams, + IdentityCreateRequest, + IdentityGetRequest, + IdentityGetByEmailRequest, + IdentityUpdateRequest, + IdentityUpdateByEmailRequest, + IdentityDeleteRequest, + IdentityDeleteByEmailRequest, +) + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncIdentitiesResource: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncIdentitiesResource(self.mock_client) + + async def test_list_identities_returns_api_response(self): + request = IdentityListRequest(query_params=IdentityListQueryParams()) + result = await self.resource.list_identities(request) + assert isinstance(result, APIResponse) + + async def test_list_identities_calls_correct_endpoint(self): + request = IdentityListRequest(query_params=IdentityListQueryParams()) + await self.resource.list_identities(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "identities" + assert call.kwargs["params"] == {"page": 1, "limit": 25} + + async def test_create_identity_returns_api_response(self): + request = IdentityCreateRequest( + domain_id="dom123", name="John", email="john@example.com" + ) + result = await self.resource.create_identity(request) + assert isinstance(result, APIResponse) + + async def test_create_identity_calls_correct_endpoint(self): + request = IdentityCreateRequest( + domain_id="dom123", name="John", email="john@example.com" + ) + await self.resource.create_identity(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "POST" + assert call.kwargs["path"] == "identities" + assert call.kwargs["body"]["email"] == "john@example.com" + + async def test_get_identity_returns_api_response(self): + result = await self.resource.get_identity( + IdentityGetRequest(identity_id="id123") + ) + assert isinstance(result, APIResponse) + + async def test_get_identity_calls_correct_endpoint(self): + await self.resource.get_identity(IdentityGetRequest(identity_id="id123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "identities/id123" + + async def test_get_identity_by_email_returns_api_response(self): + result = await self.resource.get_identity_by_email( + IdentityGetByEmailRequest(email="john@example.com") + ) + assert isinstance(result, APIResponse) + + async def test_get_identity_by_email_calls_correct_endpoint(self): + await self.resource.get_identity_by_email( + IdentityGetByEmailRequest(email="john@example.com") + ) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "identities/email/john@example.com" + + async def test_update_identity_returns_api_response(self): + request = IdentityUpdateRequest(identity_id="id123", name="Updated") + result = await self.resource.update_identity(request) + assert isinstance(result, APIResponse) + + async def test_update_identity_excludes_id_from_body(self): + request = IdentityUpdateRequest(identity_id="id123", name="Updated") + await self.resource.update_identity(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "PUT" + assert call.kwargs["path"] == "identities/id123" + assert "identity_id" not in (call.kwargs.get("body") or {}) + + async def test_update_identity_by_email_returns_api_response(self): + request = IdentityUpdateByEmailRequest(email="john@example.com", name="Updated") + result = await self.resource.update_identity_by_email(request) + assert isinstance(result, APIResponse) + + async def test_update_identity_by_email_excludes_email_from_body(self): + request = IdentityUpdateByEmailRequest(email="john@example.com", name="Updated") + await self.resource.update_identity_by_email(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "PUT" + assert call.kwargs["path"] == "identities/email/john@example.com" + assert "email" not in (call.kwargs.get("body") or {}) + + async def test_delete_identity_returns_api_response(self): + result = await self.resource.delete_identity( + IdentityDeleteRequest(identity_id="id123") + ) + assert isinstance(result, APIResponse) + + async def test_delete_identity_calls_correct_endpoint(self): + await self.resource.delete_identity(IdentityDeleteRequest(identity_id="id123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "identities/id123" + + async def test_delete_identity_by_email_returns_api_response(self): + result = await self.resource.delete_identity_by_email( + IdentityDeleteByEmailRequest(email="john@example.com") + ) + assert isinstance(result, APIResponse) + + async def test_delete_identity_by_email_calls_correct_endpoint(self): + await self.resource.delete_identity_by_email( + IdentityDeleteByEmailRequest(email="john@example.com") + ) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "identities/email/john@example.com" diff --git a/tests/unit/test_async_inbound_resource.py b/tests/unit/test_async_inbound_resource.py new file mode 100644 index 0000000..b8a679c --- /dev/null +++ b/tests/unit/test_async_inbound_resource.py @@ -0,0 +1,124 @@ +"""Tests for AsyncInboundResource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.inbound import AsyncInboundResource +from mailersend.models.base import APIResponse +from mailersend.models.inbound import ( + InboundListRequest, + InboundListQueryParams, + InboundGetRequest, + InboundCreateRequest, + InboundUpdateRequest, + InboundDeleteRequest, + InboundFilterGroup, + InboundForward, +) + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +def _make_filter_group(): + return InboundFilterGroup(type="catch_all") + + +def _make_forward(): + return InboundForward(type="email", value="forward@example.com") + + +class TestAsyncInboundResource: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncInboundResource(self.mock_client) + + async def test_list_returns_api_response(self): + request = InboundListRequest(query_params=InboundListQueryParams()) + result = await self.resource.list(request) + assert isinstance(result, APIResponse) + + async def test_list_calls_correct_endpoint(self): + request = InboundListRequest(query_params=InboundListQueryParams()) + await self.resource.list(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "inbound" + + async def test_get_returns_api_response(self): + result = await self.resource.get(InboundGetRequest(inbound_id="inb123")) + assert isinstance(result, APIResponse) + + async def test_get_calls_correct_endpoint(self): + await self.resource.get(InboundGetRequest(inbound_id="inb123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "inbound/inb123" + + async def test_create_returns_api_response(self): + request = InboundCreateRequest( + domain_id="dom123", + name="Test Route", + domain_enabled=False, + catch_filter=_make_filter_group(), + match_filter=_make_filter_group(), + forwards=[_make_forward()], + ) + result = await self.resource.create(request) + assert isinstance(result, APIResponse) + + async def test_create_calls_correct_endpoint(self): + request = InboundCreateRequest( + domain_id="dom123", + name="Test Route", + domain_enabled=False, + catch_filter=_make_filter_group(), + match_filter=_make_filter_group(), + forwards=[_make_forward()], + ) + await self.resource.create(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "POST" + assert call.kwargs["path"] == "inbound" + + async def test_update_returns_api_response(self): + request = InboundUpdateRequest( + inbound_id="inb123", + name="Updated Route", + domain_enabled=False, + catch_filter=_make_filter_group(), + match_filter=_make_filter_group(), + forwards=[_make_forward()], + ) + result = await self.resource.update(request) + assert isinstance(result, APIResponse) + + async def test_update_calls_correct_endpoint(self): + request = InboundUpdateRequest( + inbound_id="inb123", + name="Updated Route", + domain_enabled=False, + catch_filter=_make_filter_group(), + match_filter=_make_filter_group(), + forwards=[_make_forward()], + ) + await self.resource.update(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "PUT" + assert call.kwargs["path"] == "inbound/inb123" + + async def test_delete_returns_api_response(self): + result = await self.resource.delete(InboundDeleteRequest(inbound_id="inb123")) + assert isinstance(result, APIResponse) + + async def test_delete_calls_correct_endpoint(self): + await self.resource.delete(InboundDeleteRequest(inbound_id="inb123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "inbound/inb123" diff --git a/tests/unit/test_async_messages_resource.py b/tests/unit/test_async_messages_resource.py new file mode 100644 index 0000000..29820b2 --- /dev/null +++ b/tests/unit/test_async_messages_resource.py @@ -0,0 +1,58 @@ +"""Tests for AsyncMessages resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.messages import AsyncMessages +from mailersend.models.base import APIResponse +from mailersend.models.messages import ( + MessagesListRequest, + MessagesListQueryParams, + MessageGetRequest, +) + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncMessages: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncMessages(self.mock_client) + + async def test_list_messages_returns_api_response(self): + request = MessagesListRequest(query_params=MessagesListQueryParams()) + result = await self.resource.list_messages(request) + assert isinstance(result, APIResponse) + + async def test_list_messages_calls_correct_endpoint(self): + request = MessagesListRequest(query_params=MessagesListQueryParams()) + await self.resource.list_messages(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "messages" + + async def test_list_messages_passes_query_params(self): + request = MessagesListRequest( + query_params=MessagesListQueryParams(page=2, limit=10) + ) + await self.resource.list_messages(request) + call = self.mock_client.request.call_args + assert call.kwargs["params"]["page"] == 2 + assert call.kwargs["params"]["limit"] == 10 + + async def test_get_message_returns_api_response(self): + result = await self.resource.get_message(MessageGetRequest(message_id="msg123")) + assert isinstance(result, APIResponse) + + async def test_get_message_calls_correct_endpoint(self): + await self.resource.get_message(MessageGetRequest(message_id="msg123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "messages/msg123" diff --git a/tests/unit/test_async_other_resource.py b/tests/unit/test_async_other_resource.py new file mode 100644 index 0000000..a00f5db --- /dev/null +++ b/tests/unit/test_async_other_resource.py @@ -0,0 +1,32 @@ +"""Tests for AsyncOther resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.other import AsyncOther +from mailersend.models.base import APIResponse + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncOther: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncOther(self.mock_client) + + async def test_get_quota_returns_api_response(self): + result = await self.resource.get_quota() + assert isinstance(result, APIResponse) + + async def test_get_quota_calls_correct_endpoint(self): + await self.resource.get_quota() + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "api-quota" diff --git a/tests/unit/test_async_recipients_resource.py b/tests/unit/test_async_recipients_resource.py new file mode 100644 index 0000000..e0785e5 --- /dev/null +++ b/tests/unit/test_async_recipients_resource.py @@ -0,0 +1,282 @@ +"""Tests for AsyncRecipients resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.recipients import AsyncRecipients +from mailersend.models.base import APIResponse +from mailersend.models.recipients import ( + RecipientsListRequest, + RecipientsListQueryParams, + RecipientGetRequest, + RecipientDeleteRequest, + SuppressionListRequest, + SuppressionListQueryParams, + SuppressionAddRequest, + SuppressionDeleteRequest, +) + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncRecipients: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncRecipients(self.mock_client) + + async def test_list_recipients_returns_api_response(self): + result = await self.resource.list_recipients() + assert isinstance(result, APIResponse) + + async def test_list_recipients_calls_correct_endpoint(self): + await self.resource.list_recipients() + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "recipients" + + async def test_list_recipients_with_request(self): + request = RecipientsListRequest( + query_params=RecipientsListQueryParams(page=2, limit=10) + ) + await self.resource.list_recipients(request) + call = self.mock_client.request.call_args + assert call.kwargs["params"]["page"] == 2 + + async def test_get_recipient_returns_api_response(self): + result = await self.resource.get_recipient( + RecipientGetRequest(recipient_id="rec123") + ) + assert isinstance(result, APIResponse) + + async def test_get_recipient_calls_correct_endpoint(self): + await self.resource.get_recipient(RecipientGetRequest(recipient_id="rec123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "recipients/rec123" + + async def test_delete_recipient_returns_api_response(self): + result = await self.resource.delete_recipient( + RecipientDeleteRequest(recipient_id="rec123") + ) + assert isinstance(result, APIResponse) + + async def test_delete_recipient_calls_correct_endpoint(self): + await self.resource.delete_recipient( + RecipientDeleteRequest(recipient_id="rec123") + ) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "recipients/rec123" + + async def test_list_blocklist_returns_api_response(self): + result = await self.resource.list_blocklist() + assert isinstance(result, APIResponse) + + async def test_list_blocklist_calls_correct_endpoint(self): + await self.resource.list_blocklist() + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "suppressions/blocklist" + + async def test_list_hard_bounces_returns_api_response(self): + result = await self.resource.list_hard_bounces() + assert isinstance(result, APIResponse) + + async def test_list_hard_bounces_calls_correct_endpoint(self): + await self.resource.list_hard_bounces() + call = self.mock_client.request.call_args + assert call.kwargs["path"] == "suppressions/hard-bounces" + + async def test_list_spam_complaints_returns_api_response(self): + result = await self.resource.list_spam_complaints() + assert isinstance(result, APIResponse) + + async def test_list_spam_complaints_calls_correct_endpoint(self): + await self.resource.list_spam_complaints() + call = self.mock_client.request.call_args + assert call.kwargs["path"] == "suppressions/spam-complaints" + + async def test_list_unsubscribes_returns_api_response(self): + result = await self.resource.list_unsubscribes() + assert isinstance(result, APIResponse) + + async def test_list_unsubscribes_calls_correct_endpoint(self): + await self.resource.list_unsubscribes() + call = self.mock_client.request.call_args + assert call.kwargs["path"] == "suppressions/unsubscribes" + + async def test_list_on_hold_returns_api_response(self): + result = await self.resource.list_on_hold() + assert isinstance(result, APIResponse) + + async def test_list_on_hold_calls_correct_endpoint(self): + await self.resource.list_on_hold() + call = self.mock_client.request.call_args + assert call.kwargs["path"] == "suppressions/on-hold-list" + + async def test_add_to_blocklist_returns_api_response(self): + request = SuppressionAddRequest( + domain_id="dom123", recipients=["a@example.com"] + ) + result = await self.resource.add_to_blocklist(request) + assert isinstance(result, APIResponse) + + async def test_add_to_blocklist_calls_correct_endpoint(self): + request = SuppressionAddRequest( + domain_id="dom123", recipients=["a@example.com"] + ) + await self.resource.add_to_blocklist(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "POST" + assert call.kwargs["path"] == "suppressions/blocklist" + + async def test_add_hard_bounces_returns_api_response(self): + request = SuppressionAddRequest( + domain_id="dom123", recipients=["a@example.com"] + ) + result = await self.resource.add_hard_bounces(request) + assert isinstance(result, APIResponse) + + async def test_add_hard_bounces_calls_correct_endpoint(self): + request = SuppressionAddRequest( + domain_id="dom123", recipients=["a@example.com"] + ) + await self.resource.add_hard_bounces(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "POST" + assert call.kwargs["path"] == "suppressions/hard-bounces" + + async def test_add_spam_complaints_returns_api_response(self): + request = SuppressionAddRequest( + domain_id="dom123", recipients=["a@example.com"] + ) + result = await self.resource.add_spam_complaints(request) + assert isinstance(result, APIResponse) + + async def test_add_spam_complaints_calls_correct_endpoint(self): + request = SuppressionAddRequest( + domain_id="dom123", recipients=["a@example.com"] + ) + await self.resource.add_spam_complaints(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "POST" + assert call.kwargs["path"] == "suppressions/spam-complaints" + + async def test_add_unsubscribes_returns_api_response(self): + request = SuppressionAddRequest( + domain_id="dom123", recipients=["a@example.com"] + ) + result = await self.resource.add_unsubscribes(request) + assert isinstance(result, APIResponse) + + async def test_add_unsubscribes_calls_correct_endpoint(self): + request = SuppressionAddRequest( + domain_id="dom123", recipients=["a@example.com"] + ) + await self.resource.add_unsubscribes(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "POST" + assert call.kwargs["path"] == "suppressions/unsubscribes" + + async def test_delete_from_blocklist_returns_api_response(self): + request = SuppressionDeleteRequest(ids=["id1"]) + result = await self.resource.delete_from_blocklist(request) + assert isinstance(result, APIResponse) + + async def test_delete_from_blocklist_calls_correct_endpoint(self): + request = SuppressionDeleteRequest(ids=["id1"]) + await self.resource.delete_from_blocklist(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "suppressions/blocklist" + + async def test_delete_hard_bounces_returns_api_response(self): + result = await self.resource.delete_hard_bounces( + SuppressionDeleteRequest(ids=["id1"]) + ) + assert isinstance(result, APIResponse) + + async def test_delete_hard_bounces_calls_correct_endpoint(self): + await self.resource.delete_hard_bounces(SuppressionDeleteRequest(ids=["id1"])) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "suppressions/hard-bounces" + + async def test_delete_spam_complaints_returns_api_response(self): + result = await self.resource.delete_spam_complaints( + SuppressionDeleteRequest(ids=["id1"]) + ) + assert isinstance(result, APIResponse) + + async def test_delete_spam_complaints_calls_correct_endpoint(self): + await self.resource.delete_spam_complaints( + SuppressionDeleteRequest(ids=["id1"]) + ) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "suppressions/spam-complaints" + + async def test_delete_unsubscribes_returns_api_response(self): + result = await self.resource.delete_unsubscribes( + SuppressionDeleteRequest(ids=["id1"]) + ) + assert isinstance(result, APIResponse) + + async def test_delete_unsubscribes_calls_correct_endpoint(self): + await self.resource.delete_unsubscribes(SuppressionDeleteRequest(ids=["id1"])) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "suppressions/unsubscribes" + + async def test_delete_from_on_hold_returns_api_response(self): + result = await self.resource.delete_from_on_hold( + SuppressionDeleteRequest(ids=["id1"]) + ) + assert isinstance(result, APIResponse) + + async def test_delete_from_on_hold_calls_correct_endpoint(self): + await self.resource.delete_from_on_hold(SuppressionDeleteRequest(ids=["id1"])) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "suppressions/on-hold-list" + + async def test_delete_hard_bounces_excludes_domain_id_from_body(self): + request = SuppressionDeleteRequest(domain_id="dom123", ids=["id1"]) + await self.resource.delete_hard_bounces(request) + body = self.mock_client.request.call_args.kwargs.get("body") or {} + assert "domain_id" not in body + assert body.get("ids") == ["id1"] + + async def test_delete_spam_complaints_excludes_domain_id_from_body(self): + request = SuppressionDeleteRequest(domain_id="dom123", ids=["id1"]) + await self.resource.delete_spam_complaints(request) + body = self.mock_client.request.call_args.kwargs.get("body") or {} + assert "domain_id" not in body + assert body.get("ids") == ["id1"] + + async def test_delete_unsubscribes_excludes_domain_id_from_body(self): + request = SuppressionDeleteRequest(domain_id="dom123", ids=["id1"]) + await self.resource.delete_unsubscribes(request) + body = self.mock_client.request.call_args.kwargs.get("body") or {} + assert "domain_id" not in body + assert body.get("ids") == ["id1"] + + async def test_delete_from_on_hold_excludes_domain_id_from_body(self): + request = SuppressionDeleteRequest(domain_id="dom123", ids=["id1"]) + await self.resource.delete_from_on_hold(request) + body = self.mock_client.request.call_args.kwargs.get("body") or {} + assert "domain_id" not in body + assert body.get("ids") == ["id1"] + + async def test_delete_from_blocklist_includes_domain_id_in_body(self): + request = SuppressionDeleteRequest(domain_id="dom123", ids=["id1"]) + await self.resource.delete_from_blocklist(request) + body = self.mock_client.request.call_args.kwargs.get("body") or {} + assert body.get("domain_id") == "dom123" diff --git a/tests/unit/test_async_schedules_resource.py b/tests/unit/test_async_schedules_resource.py new file mode 100644 index 0000000..23c30be --- /dev/null +++ b/tests/unit/test_async_schedules_resource.py @@ -0,0 +1,64 @@ +"""Tests for AsyncSchedules resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.schedules import AsyncSchedules +from mailersend.models.base import APIResponse +from mailersend.models.schedules import ( + SchedulesListRequest, + SchedulesListQueryParams, + ScheduleGetRequest, + ScheduleDeleteRequest, +) + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncSchedules: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncSchedules(self.mock_client) + + async def test_list_schedules_returns_api_response(self): + request = SchedulesListRequest(query_params=SchedulesListQueryParams()) + result = await self.resource.list_schedules(request) + assert isinstance(result, APIResponse) + + async def test_list_schedules_calls_correct_endpoint(self): + request = SchedulesListRequest(query_params=SchedulesListQueryParams()) + await self.resource.list_schedules(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "message-schedules" + + async def test_get_schedule_returns_api_response(self): + result = await self.resource.get_schedule( + ScheduleGetRequest(message_id="msg123") + ) + assert isinstance(result, APIResponse) + + async def test_get_schedule_calls_correct_endpoint(self): + await self.resource.get_schedule(ScheduleGetRequest(message_id="msg123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "message-schedules/msg123" + + async def test_delete_schedule_returns_api_response(self): + result = await self.resource.delete_schedule( + ScheduleDeleteRequest(message_id="msg123") + ) + assert isinstance(result, APIResponse) + + async def test_delete_schedule_calls_correct_endpoint(self): + await self.resource.delete_schedule(ScheduleDeleteRequest(message_id="msg123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "message-schedules/msg123" diff --git a/tests/unit/test_async_sms_activity_resource.py b/tests/unit/test_async_sms_activity_resource.py new file mode 100644 index 0000000..7cf6ca9 --- /dev/null +++ b/tests/unit/test_async_sms_activity_resource.py @@ -0,0 +1,46 @@ +"""Tests for AsyncSmsActivity resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.sms_activity import AsyncSmsActivity +from mailersend.models.base import APIResponse +from mailersend.models.sms_activity import ( + SmsActivityListRequest, + SmsMessageGetRequest, +) + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncSmsActivity: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncSmsActivity(self.mock_client) + + async def test_list_returns_api_response(self): + result = await self.resource.list(SmsActivityListRequest()) + assert isinstance(result, APIResponse) + + async def test_list_calls_correct_endpoint(self): + await self.resource.list(SmsActivityListRequest()) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "sms-activity" + + async def test_get_returns_api_response(self): + result = await self.resource.get(SmsMessageGetRequest(sms_message_id="msg123")) + assert isinstance(result, APIResponse) + + async def test_get_calls_correct_endpoint(self): + await self.resource.get(SmsMessageGetRequest(sms_message_id="msg123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "sms-messages/msg123" diff --git a/tests/unit/test_async_sms_inbounds_resource.py b/tests/unit/test_async_sms_inbounds_resource.py new file mode 100644 index 0000000..801a720 --- /dev/null +++ b/tests/unit/test_async_sms_inbounds_resource.py @@ -0,0 +1,100 @@ +"""Tests for AsyncSmsInbounds resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.sms_inbounds import AsyncSmsInbounds +from mailersend.models.base import APIResponse +from mailersend.models.sms_inbounds import ( + SmsInboundsListRequest, + SmsInboundsListQueryParams, + SmsInboundGetRequest, + SmsInboundCreateRequest, + SmsInboundUpdateRequest, + SmsInboundDeleteRequest, +) + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncSmsInbounds: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncSmsInbounds(self.mock_client) + + async def test_list_sms_inbounds_returns_api_response(self): + result = await self.resource.list_sms_inbounds(SmsInboundsListRequest()) + assert isinstance(result, APIResponse) + + async def test_list_sms_inbounds_calls_correct_endpoint(self): + await self.resource.list_sms_inbounds(SmsInboundsListRequest()) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "sms-inbounds" + + async def test_get_sms_inbound_returns_api_response(self): + result = await self.resource.get_sms_inbound( + SmsInboundGetRequest(sms_inbound_id="inb123") + ) + assert isinstance(result, APIResponse) + + async def test_get_sms_inbound_calls_correct_endpoint(self): + await self.resource.get_sms_inbound( + SmsInboundGetRequest(sms_inbound_id="inb123") + ) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "sms-inbounds/inb123" + + async def test_create_sms_inbound_returns_api_response(self): + request = SmsInboundCreateRequest( + sms_number_id="num123", + name="My Inbound", + forward_url="https://example.com/webhook", + ) + result = await self.resource.create_sms_inbound(request) + assert isinstance(result, APIResponse) + + async def test_create_sms_inbound_calls_correct_endpoint(self): + request = SmsInboundCreateRequest( + sms_number_id="num123", + name="My Inbound", + forward_url="https://example.com/webhook", + ) + await self.resource.create_sms_inbound(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "POST" + assert call.kwargs["path"] == "sms-inbounds" + + async def test_update_sms_inbound_returns_api_response(self): + request = SmsInboundUpdateRequest(sms_inbound_id="inb123", name="Updated") + result = await self.resource.update_sms_inbound(request) + assert isinstance(result, APIResponse) + + async def test_update_sms_inbound_calls_correct_endpoint(self): + request = SmsInboundUpdateRequest(sms_inbound_id="inb123", name="Updated") + await self.resource.update_sms_inbound(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "PUT" + assert call.kwargs["path"] == "sms-inbounds/inb123" + + async def test_delete_sms_inbound_returns_api_response(self): + result = await self.resource.delete_sms_inbound( + SmsInboundDeleteRequest(sms_inbound_id="inb123") + ) + assert isinstance(result, APIResponse) + + async def test_delete_sms_inbound_calls_correct_endpoint(self): + await self.resource.delete_sms_inbound( + SmsInboundDeleteRequest(sms_inbound_id="inb123") + ) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "sms-inbounds/inb123" diff --git a/tests/unit/test_async_sms_messages_resource.py b/tests/unit/test_async_sms_messages_resource.py new file mode 100644 index 0000000..488c0b4 --- /dev/null +++ b/tests/unit/test_async_sms_messages_resource.py @@ -0,0 +1,60 @@ +"""Tests for AsyncSmsMessages resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.sms_messages import AsyncSmsMessages +from mailersend.models.base import APIResponse +from mailersend.models.sms_messages import ( + SmsMessagesListRequest, + SmsMessagesListQueryParams, + SmsMessageGetRequest, +) + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncSmsMessages: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncSmsMessages(self.mock_client) + + async def test_list_sms_messages_returns_api_response(self): + result = await self.resource.list_sms_messages(SmsMessagesListRequest()) + assert isinstance(result, APIResponse) + + async def test_list_sms_messages_calls_correct_endpoint(self): + await self.resource.list_sms_messages(SmsMessagesListRequest()) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "sms-messages" + + async def test_list_sms_messages_passes_query_params(self): + request = SmsMessagesListRequest( + query_params=SmsMessagesListQueryParams(page=2, limit=10) + ) + await self.resource.list_sms_messages(request) + call = self.mock_client.request.call_args + assert call.kwargs["params"]["page"] == 2 + assert call.kwargs["params"]["limit"] == 10 + + async def test_get_sms_message_returns_api_response(self): + result = await self.resource.get_sms_message( + SmsMessageGetRequest(sms_message_id="msg123") + ) + assert isinstance(result, APIResponse) + + async def test_get_sms_message_calls_correct_endpoint(self): + await self.resource.get_sms_message( + SmsMessageGetRequest(sms_message_id="msg123") + ) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "sms-messages/msg123" diff --git a/tests/unit/test_async_sms_numbers_resource.py b/tests/unit/test_async_sms_numbers_resource.py new file mode 100644 index 0000000..5cffa9c --- /dev/null +++ b/tests/unit/test_async_sms_numbers_resource.py @@ -0,0 +1,74 @@ +"""Tests for AsyncSmsNumbers resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.sms_numbers import AsyncSmsNumbers +from mailersend.models.base import APIResponse +from mailersend.models.sms_numbers import ( + SmsNumbersListRequest, + SmsNumberGetRequest, + SmsNumberUpdateRequest, + SmsNumberDeleteRequest, +) + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncSmsNumbers: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncSmsNumbers(self.mock_client) + + async def test_list_returns_api_response(self): + result = await self.resource.list(SmsNumbersListRequest()) + assert isinstance(result, APIResponse) + + async def test_list_calls_correct_endpoint(self): + await self.resource.list(SmsNumbersListRequest()) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "sms-numbers" + + async def test_get_returns_api_response(self): + result = await self.resource.get(SmsNumberGetRequest(sms_number_id="num123")) + assert isinstance(result, APIResponse) + + async def test_get_calls_correct_endpoint(self): + await self.resource.get(SmsNumberGetRequest(sms_number_id="num123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "sms-numbers/num123" + + async def test_update_returns_api_response(self): + result = await self.resource.update( + SmsNumberUpdateRequest(sms_number_id="num123", paused=True) + ) + assert isinstance(result, APIResponse) + + async def test_update_calls_correct_endpoint(self): + await self.resource.update( + SmsNumberUpdateRequest(sms_number_id="num123", paused=True) + ) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "PUT" + assert call.kwargs["path"] == "sms-numbers/num123" + + async def test_delete_returns_api_response(self): + result = await self.resource.delete( + SmsNumberDeleteRequest(sms_number_id="num123") + ) + assert isinstance(result, APIResponse) + + async def test_delete_calls_correct_endpoint(self): + await self.resource.delete(SmsNumberDeleteRequest(sms_number_id="num123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "sms-numbers/num123" diff --git a/tests/unit/test_async_sms_recipients_resource.py b/tests/unit/test_async_sms_recipients_resource.py new file mode 100644 index 0000000..42d4608 --- /dev/null +++ b/tests/unit/test_async_sms_recipients_resource.py @@ -0,0 +1,77 @@ +"""Tests for AsyncSmsRecipients resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.sms_recipients import AsyncSmsRecipients +from mailersend.models.base import APIResponse +from mailersend.models.sms_recipients import ( + SmsRecipientsListRequest, + SmsRecipientsListQueryParams, + SmsRecipientGetRequest, + SmsRecipientUpdateRequest, + SmsRecipientStatus, +) + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncSmsRecipients: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncSmsRecipients(self.mock_client) + + async def test_list_sms_recipients_returns_api_response(self): + result = await self.resource.list_sms_recipients(SmsRecipientsListRequest()) + assert isinstance(result, APIResponse) + + async def test_list_sms_recipients_calls_correct_endpoint(self): + await self.resource.list_sms_recipients(SmsRecipientsListRequest()) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "sms-recipients" + + async def test_list_sms_recipients_with_custom_params(self): + request = SmsRecipientsListRequest( + query_params=SmsRecipientsListQueryParams(page=2) + ) + await self.resource.list_sms_recipients(request) + call = self.mock_client.request.call_args + assert call.kwargs["params"]["page"] == 2 + + async def test_get_sms_recipient_returns_api_response(self): + result = await self.resource.get_sms_recipient( + SmsRecipientGetRequest(sms_recipient_id="rec123") + ) + assert isinstance(result, APIResponse) + + async def test_get_sms_recipient_calls_correct_endpoint(self): + await self.resource.get_sms_recipient( + SmsRecipientGetRequest(sms_recipient_id="rec123") + ) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "sms-recipients/rec123" + + async def test_update_sms_recipient_returns_api_response(self): + request = SmsRecipientUpdateRequest( + sms_recipient_id="rec123", status=SmsRecipientStatus.ACTIVE + ) + result = await self.resource.update_sms_recipient(request) + assert isinstance(result, APIResponse) + + async def test_update_sms_recipient_calls_correct_endpoint(self): + request = SmsRecipientUpdateRequest( + sms_recipient_id="rec123", status=SmsRecipientStatus.ACTIVE + ) + await self.resource.update_sms_recipient(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "PUT" + assert call.kwargs["path"] == "sms-recipients/rec123" diff --git a/tests/unit/test_async_sms_sending_resource.py b/tests/unit/test_async_sms_sending_resource.py new file mode 100644 index 0000000..2ae313c --- /dev/null +++ b/tests/unit/test_async_sms_sending_resource.py @@ -0,0 +1,43 @@ +"""Tests for AsyncSmsSending resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.sms_sending import AsyncSmsSending +from mailersend.models.base import APIResponse +from mailersend.models.sms_sending import SmsSendRequest + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncSmsSending: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncSmsSending(self.mock_client) + + async def test_send_returns_api_response(self): + request = SmsSendRequest( + from_number="+15551234567", + to=["+15559876543"], + text="Hello from tests", + ) + result = await self.resource.send(request) + assert isinstance(result, APIResponse) + + async def test_send_calls_correct_endpoint(self): + request = SmsSendRequest( + from_number="+15551234567", + to=["+15559876543"], + text="Hello from tests", + ) + await self.resource.send(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "POST" + assert call.kwargs["path"] == "sms" diff --git a/tests/unit/test_async_sms_webhooks_resource.py b/tests/unit/test_async_sms_webhooks_resource.py new file mode 100644 index 0000000..8004c71 --- /dev/null +++ b/tests/unit/test_async_sms_webhooks_resource.py @@ -0,0 +1,109 @@ +"""Tests for AsyncSmsWebhooks resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.sms_webhooks import AsyncSmsWebhooks +from mailersend.models.base import APIResponse +from mailersend.models.sms_webhooks import ( + SmsWebhooksListRequest, + SmsWebhooksListQueryParams, + SmsWebhookGetRequest, + SmsWebhookCreateRequest, + SmsWebhookUpdateRequest, + SmsWebhookDeleteRequest, + SmsWebhookEvent, +) + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncSmsWebhooks: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncSmsWebhooks(self.mock_client) + + async def test_list_sms_webhooks_returns_api_response(self): + request = SmsWebhooksListRequest( + query_params=SmsWebhooksListQueryParams(sms_number_id="num123") + ) + result = await self.resource.list_sms_webhooks(request) + assert isinstance(result, APIResponse) + + async def test_list_sms_webhooks_calls_correct_endpoint(self): + request = SmsWebhooksListRequest( + query_params=SmsWebhooksListQueryParams(sms_number_id="num123") + ) + await self.resource.list_sms_webhooks(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "sms-webhooks" + + async def test_get_sms_webhook_returns_api_response(self): + result = await self.resource.get_sms_webhook( + SmsWebhookGetRequest(sms_webhook_id="wh123") + ) + assert isinstance(result, APIResponse) + + async def test_get_sms_webhook_calls_correct_endpoint(self): + await self.resource.get_sms_webhook( + SmsWebhookGetRequest(sms_webhook_id="wh123") + ) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "sms-webhooks/wh123" + + async def test_create_sms_webhook_returns_api_response(self): + request = SmsWebhookCreateRequest( + url="https://example.com/webhook", + name="My Webhook", + events=[SmsWebhookEvent.SMS_SENT], + sms_number_id="num123", + ) + result = await self.resource.create_sms_webhook(request) + assert isinstance(result, APIResponse) + + async def test_create_sms_webhook_calls_correct_endpoint(self): + request = SmsWebhookCreateRequest( + url="https://example.com/webhook", + name="My Webhook", + events=[SmsWebhookEvent.SMS_SENT], + sms_number_id="num123", + ) + await self.resource.create_sms_webhook(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "POST" + assert call.kwargs["path"] == "sms-webhooks" + + async def test_update_sms_webhook_returns_api_response(self): + request = SmsWebhookUpdateRequest(sms_webhook_id="wh123", name="Updated") + result = await self.resource.update_sms_webhook(request) + assert isinstance(result, APIResponse) + + async def test_update_sms_webhook_calls_correct_endpoint(self): + request = SmsWebhookUpdateRequest(sms_webhook_id="wh123", name="Updated") + await self.resource.update_sms_webhook(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "PUT" + assert call.kwargs["path"] == "sms-webhooks/wh123" + + async def test_delete_sms_webhook_returns_api_response(self): + result = await self.resource.delete_sms_webhook( + SmsWebhookDeleteRequest(sms_webhook_id="wh123") + ) + assert isinstance(result, APIResponse) + + async def test_delete_sms_webhook_calls_correct_endpoint(self): + await self.resource.delete_sms_webhook( + SmsWebhookDeleteRequest(sms_webhook_id="wh123") + ) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "sms-webhooks/wh123" diff --git a/tests/unit/test_async_smtp_users_resource.py b/tests/unit/test_async_smtp_users_resource.py new file mode 100644 index 0000000..5502678 --- /dev/null +++ b/tests/unit/test_async_smtp_users_resource.py @@ -0,0 +1,94 @@ +"""Tests for AsyncSmtpUsers resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.smtp_users import AsyncSmtpUsers +from mailersend.models.base import APIResponse +from mailersend.models.smtp_users import ( + SmtpUsersListRequest, + SmtpUsersListQueryParams, + SmtpUserGetRequest, + SmtpUserCreateRequest, + SmtpUserUpdateRequest, + SmtpUserDeleteRequest, +) + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncSmtpUsers: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncSmtpUsers(self.mock_client) + + async def test_list_smtp_users_returns_api_response(self): + request = SmtpUsersListRequest(domain_id="dom123") + result = await self.resource.list_smtp_users(request) + assert isinstance(result, APIResponse) + + async def test_list_smtp_users_calls_correct_endpoint(self): + request = SmtpUsersListRequest(domain_id="dom123") + await self.resource.list_smtp_users(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "domains/dom123/smtp-users" + + async def test_get_smtp_user_returns_api_response(self): + request = SmtpUserGetRequest(domain_id="dom123", smtp_user_id="user456") + result = await self.resource.get_smtp_user(request) + assert isinstance(result, APIResponse) + + async def test_get_smtp_user_calls_correct_endpoint(self): + request = SmtpUserGetRequest(domain_id="dom123", smtp_user_id="user456") + await self.resource.get_smtp_user(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "domains/dom123/smtp-users/user456" + + async def test_create_smtp_user_returns_api_response(self): + request = SmtpUserCreateRequest(domain_id="dom123", name="My SMTP User") + result = await self.resource.create_smtp_user(request) + assert isinstance(result, APIResponse) + + async def test_create_smtp_user_calls_correct_endpoint(self): + request = SmtpUserCreateRequest(domain_id="dom123", name="My SMTP User") + await self.resource.create_smtp_user(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "POST" + assert call.kwargs["path"] == "domains/dom123/smtp-users" + + async def test_update_smtp_user_returns_api_response(self): + request = SmtpUserUpdateRequest( + domain_id="dom123", smtp_user_id="user456", name="Updated" + ) + result = await self.resource.update_smtp_user(request) + assert isinstance(result, APIResponse) + + async def test_update_smtp_user_calls_correct_endpoint(self): + request = SmtpUserUpdateRequest( + domain_id="dom123", smtp_user_id="user456", name="Updated" + ) + await self.resource.update_smtp_user(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "PUT" + assert call.kwargs["path"] == "domains/dom123/smtp-users/user456" + + async def test_delete_smtp_user_returns_api_response(self): + request = SmtpUserDeleteRequest(domain_id="dom123", smtp_user_id="user456") + result = await self.resource.delete_smtp_user(request) + assert isinstance(result, APIResponse) + + async def test_delete_smtp_user_calls_correct_endpoint(self): + request = SmtpUserDeleteRequest(domain_id="dom123", smtp_user_id="user456") + await self.resource.delete_smtp_user(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "domains/dom123/smtp-users/user456" diff --git a/tests/unit/test_async_templates_resource.py b/tests/unit/test_async_templates_resource.py new file mode 100644 index 0000000..2680bfa --- /dev/null +++ b/tests/unit/test_async_templates_resource.py @@ -0,0 +1,72 @@ +"""Tests for AsyncTemplates resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.templates import AsyncTemplates +from mailersend.models.base import APIResponse +from mailersend.models.templates import ( + TemplatesListRequest, + TemplatesListQueryParams, + TemplateGetRequest, + TemplateDeleteRequest, +) + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncTemplates: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncTemplates(self.mock_client) + + async def test_list_templates_returns_api_response(self): + result = await self.resource.list_templates() + assert isinstance(result, APIResponse) + + async def test_list_templates_calls_correct_endpoint(self): + await self.resource.list_templates() + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "templates" + + async def test_list_templates_with_request(self): + request = TemplatesListRequest( + query_params=TemplatesListQueryParams(page=2, limit=10) + ) + await self.resource.list_templates(request) + call = self.mock_client.request.call_args + assert call.kwargs["params"]["page"] == 2 + + async def test_get_template_returns_api_response(self): + result = await self.resource.get_template( + TemplateGetRequest(template_id="tmpl123") + ) + assert isinstance(result, APIResponse) + + async def test_get_template_calls_correct_endpoint(self): + await self.resource.get_template(TemplateGetRequest(template_id="tmpl123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "templates/tmpl123" + + async def test_delete_template_returns_api_response(self): + result = await self.resource.delete_template( + TemplateDeleteRequest(template_id="tmpl123") + ) + assert isinstance(result, APIResponse) + + async def test_delete_template_calls_correct_endpoint(self): + await self.resource.delete_template( + TemplateDeleteRequest(template_id="tmpl123") + ) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "templates/tmpl123" diff --git a/tests/unit/test_async_tokens_resource.py b/tests/unit/test_async_tokens_resource.py new file mode 100644 index 0000000..7ff5ba3 --- /dev/null +++ b/tests/unit/test_async_tokens_resource.py @@ -0,0 +1,105 @@ +"""Tests for AsyncTokens resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.tokens import AsyncTokens +from mailersend.models.base import APIResponse +from mailersend.models.tokens import ( + TokensListRequest, + TokensListQueryParams, + TokenGetRequest, + TokenCreateRequest, + TokenUpdateRequest, + TokenUpdateNameRequest, + TokenDeleteRequest, +) + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncTokens: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncTokens(self.mock_client) + + async def test_list_tokens_returns_api_response(self): + result = await self.resource.list_tokens(TokensListRequest()) + assert isinstance(result, APIResponse) + + async def test_list_tokens_calls_correct_endpoint(self): + await self.resource.list_tokens(TokensListRequest()) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "token" + + async def test_get_token_returns_api_response(self): + result = await self.resource.get_token(TokenGetRequest(token_id="tok123")) + assert isinstance(result, APIResponse) + + async def test_get_token_calls_correct_endpoint(self): + await self.resource.get_token(TokenGetRequest(token_id="tok123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "token/tok123" + + async def test_create_token_returns_api_response(self): + request = TokenCreateRequest( + name="My Token", + domain_id="dom123", + scopes=["email_full"], + ) + result = await self.resource.create_token(request) + assert isinstance(result, APIResponse) + + async def test_create_token_calls_correct_endpoint(self): + request = TokenCreateRequest( + name="My Token", + domain_id="dom123", + scopes=["email_full"], + ) + await self.resource.create_token(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "POST" + assert call.kwargs["path"] == "token" + + async def test_update_token_returns_api_response(self): + request = TokenUpdateRequest(token_id="tok123", status="pause") + result = await self.resource.update_token(request) + assert isinstance(result, APIResponse) + + async def test_update_token_calls_correct_endpoint(self): + request = TokenUpdateRequest(token_id="tok123", status="pause") + await self.resource.update_token(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "PUT" + assert call.kwargs["path"] == "token/tok123/settings" + + async def test_update_token_name_returns_api_response(self): + request = TokenUpdateNameRequest(token_id="tok123", name="New Name") + result = await self.resource.update_token_name(request) + assert isinstance(result, APIResponse) + + async def test_update_token_name_calls_correct_endpoint(self): + request = TokenUpdateNameRequest(token_id="tok123", name="New Name") + await self.resource.update_token_name(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "PUT" + assert call.kwargs["path"] == "token/tok123" + + async def test_delete_token_returns_api_response(self): + result = await self.resource.delete_token(TokenDeleteRequest(token_id="tok123")) + assert isinstance(result, APIResponse) + + async def test_delete_token_calls_correct_endpoint(self): + await self.resource.delete_token(TokenDeleteRequest(token_id="tok123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "token/tok123" diff --git a/tests/unit/test_async_users_resource.py b/tests/unit/test_async_users_resource.py new file mode 100644 index 0000000..5e77a2b --- /dev/null +++ b/tests/unit/test_async_users_resource.py @@ -0,0 +1,133 @@ +"""Tests for AsyncUsers resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.users import AsyncUsers +from mailersend.models.base import APIResponse +from mailersend.models.users import ( + UsersListRequest, + UsersListQueryParams, + UserGetRequest, + UserInviteRequest, + UserUpdateRequest, + UserDeleteRequest, + InvitesListRequest, + InvitesListQueryParams, + InviteGetRequest, + InviteResendRequest, + InviteCancelRequest, +) + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncUsers: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncUsers(self.mock_client) + + async def test_list_users_returns_api_response(self): + result = await self.resource.list_users(UsersListRequest()) + assert isinstance(result, APIResponse) + + async def test_list_users_calls_correct_endpoint(self): + await self.resource.list_users(UsersListRequest()) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "users" + + async def test_get_user_returns_api_response(self): + result = await self.resource.get_user(UserGetRequest(user_id="usr123")) + assert isinstance(result, APIResponse) + + async def test_get_user_calls_correct_endpoint(self): + await self.resource.get_user(UserGetRequest(user_id="usr123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "users/usr123" + + async def test_invite_user_returns_api_response(self): + request = UserInviteRequest(email="newuser@example.com", role="admin") + result = await self.resource.invite_user(request) + assert isinstance(result, APIResponse) + + async def test_invite_user_calls_correct_endpoint(self): + request = UserInviteRequest(email="newuser@example.com", role="admin") + await self.resource.invite_user(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "POST" + assert call.kwargs["path"] == "users" + + async def test_update_user_returns_api_response(self): + request = UserUpdateRequest(user_id="usr123", role="viewer") + result = await self.resource.update_user(request) + assert isinstance(result, APIResponse) + + async def test_update_user_calls_correct_endpoint(self): + request = UserUpdateRequest(user_id="usr123", role="viewer") + await self.resource.update_user(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "PUT" + assert call.kwargs["path"] == "users/usr123" + + async def test_delete_user_returns_api_response(self): + result = await self.resource.delete_user(UserDeleteRequest(user_id="usr123")) + assert isinstance(result, APIResponse) + + async def test_delete_user_calls_correct_endpoint(self): + await self.resource.delete_user(UserDeleteRequest(user_id="usr123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "users/usr123" + + async def test_list_invites_returns_api_response(self): + result = await self.resource.list_invites(InvitesListRequest()) + assert isinstance(result, APIResponse) + + async def test_list_invites_calls_correct_endpoint(self): + await self.resource.list_invites(InvitesListRequest()) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "invites" + + async def test_get_invite_returns_api_response(self): + result = await self.resource.get_invite(InviteGetRequest(invite_id="inv123")) + assert isinstance(result, APIResponse) + + async def test_get_invite_calls_correct_endpoint(self): + await self.resource.get_invite(InviteGetRequest(invite_id="inv123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "invites/inv123" + + async def test_resend_invite_returns_api_response(self): + result = await self.resource.resend_invite( + InviteResendRequest(invite_id="inv123") + ) + assert isinstance(result, APIResponse) + + async def test_resend_invite_calls_correct_endpoint(self): + await self.resource.resend_invite(InviteResendRequest(invite_id="inv123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "POST" + assert call.kwargs["path"] == "invites/inv123/resend" + + async def test_cancel_invite_returns_api_response(self): + result = await self.resource.cancel_invite( + InviteCancelRequest(invite_id="inv123") + ) + assert isinstance(result, APIResponse) + + async def test_cancel_invite_calls_correct_endpoint(self): + await self.resource.cancel_invite(InviteCancelRequest(invite_id="inv123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "invites/inv123" diff --git a/tests/unit/test_async_webhooks_resource.py b/tests/unit/test_async_webhooks_resource.py new file mode 100644 index 0000000..da82238 --- /dev/null +++ b/tests/unit/test_async_webhooks_resource.py @@ -0,0 +1,103 @@ +"""Tests for AsyncWebhooks resource.""" + +from unittest.mock import AsyncMock, MagicMock + +from mailersend.resources.webhooks import AsyncWebhooks +from mailersend.models.base import APIResponse +from mailersend.models.webhooks import ( + WebhooksListRequest, + WebhooksListQueryParams, + WebhookGetRequest, + WebhookCreateRequest, + WebhookUpdateRequest, + WebhookDeleteRequest, +) + + +def _make_mock_client(): + client = MagicMock() + client.request = AsyncMock( + return_value=MagicMock( + status_code=200, headers={}, json=MagicMock(return_value={}), content=b"{}" + ) + ) + return client + + +class TestAsyncWebhooks: + def setup_method(self): + self.mock_client = _make_mock_client() + self.resource = AsyncWebhooks(self.mock_client) + + async def test_list_webhooks_returns_api_response(self): + request = WebhooksListRequest( + query_params=WebhooksListQueryParams(domain_id="dom123") + ) + result = await self.resource.list_webhooks(request) + assert isinstance(result, APIResponse) + + async def test_list_webhooks_calls_correct_endpoint(self): + request = WebhooksListRequest( + query_params=WebhooksListQueryParams(domain_id="dom123") + ) + await self.resource.list_webhooks(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "webhooks" + + async def test_get_webhook_returns_api_response(self): + result = await self.resource.get_webhook(WebhookGetRequest(webhook_id="wh123")) + assert isinstance(result, APIResponse) + + async def test_get_webhook_calls_correct_endpoint(self): + await self.resource.get_webhook(WebhookGetRequest(webhook_id="wh123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "webhooks/wh123" + + async def test_create_webhook_returns_api_response(self): + request = WebhookCreateRequest( + url="https://example.com/hook", + name="My Webhook", + events=["activity.sent"], + domain_id="dom123", + ) + result = await self.resource.create_webhook(request) + assert isinstance(result, APIResponse) + + async def test_create_webhook_calls_correct_endpoint(self): + request = WebhookCreateRequest( + url="https://example.com/hook", + name="My Webhook", + events=["activity.sent"], + domain_id="dom123", + ) + await self.resource.create_webhook(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "POST" + assert call.kwargs["path"] == "webhooks" + + async def test_update_webhook_returns_api_response(self): + request = WebhookUpdateRequest(webhook_id="wh123", name="Updated") + result = await self.resource.update_webhook(request) + assert isinstance(result, APIResponse) + + async def test_update_webhook_excludes_id_from_body(self): + request = WebhookUpdateRequest(webhook_id="wh123", name="Updated") + await self.resource.update_webhook(request) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "PUT" + assert call.kwargs["path"] == "webhooks/wh123" + assert "webhook_id" not in (call.kwargs.get("body") or {}) + + async def test_delete_webhook_returns_api_response(self): + result = await self.resource.delete_webhook( + WebhookDeleteRequest(webhook_id="wh123") + ) + assert isinstance(result, APIResponse) + + async def test_delete_webhook_calls_correct_endpoint(self): + await self.resource.delete_webhook(WebhookDeleteRequest(webhook_id="wh123")) + call = self.mock_client.request.call_args + assert call.kwargs["method"] == "DELETE" + assert call.kwargs["path"] == "webhooks/wh123" diff --git a/tests/unit/test_webhooks_builder.py b/tests/unit/test_webhooks_builder.py index a0dcf61..1b427cf 100644 --- a/tests/unit/test_webhooks_builder.py +++ b/tests/unit/test_webhooks_builder.py @@ -175,7 +175,11 @@ def test_all_events(self): "recipient.on_hold_added", "recipient.on_hold_removed", ] - expected_all_events = expected_activity_events + expected_system_events + expected_recipient_events + expected_all_events = ( + expected_activity_events + + expected_system_events + + expected_recipient_events + ) assert builder._events == expected_all_events def test_method_chaining(self): From 5f96c66aa2b5ea0f052c309fe35fbbff4b814c77 Mon Sep 17 00:00:00 2001 From: rocribera Date: Tue, 26 May 2026 15:11:23 +0200 Subject: [PATCH 2/5] feat: remove deduplicated code for async --- README.md | 224 +----------- mailersend/resources/activity.py | 48 +-- mailersend/resources/analytics.py | 89 +---- mailersend/resources/base.py | 23 +- mailersend/resources/dmarc_monitoring.py | 206 +---------- mailersend/resources/domains.py | 183 +--------- mailersend/resources/email.py | 76 +--- mailersend/resources/email_verification.py | 177 +-------- mailersend/resources/identities.py | 173 +-------- mailersend/resources/inbound.py | 105 +----- mailersend/resources/messages.py | 46 +-- mailersend/resources/other.py | 19 +- mailersend/resources/recipients.py | 380 +------------------- mailersend/resources/schedules.py | 63 +--- mailersend/resources/sms_activity.py | 44 +-- mailersend/resources/sms_inbounds.py | 97 +---- mailersend/resources/sms_messages.py | 44 +-- mailersend/resources/sms_numbers.py | 84 +---- mailersend/resources/sms_recipients.py | 71 +--- mailersend/resources/sms_sending.py | 23 +- mailersend/resources/sms_webhooks.py | 101 +----- mailersend/resources/smtp_users.py | 107 +----- mailersend/resources/templates.py | 70 +--- mailersend/resources/tokens.py | 124 +------ mailersend/resources/users.py | 184 +--------- mailersend/resources/webhooks.py | 101 +----- tests/unit/test_analytics_resource.py | 18 +- tests/unit/test_async_analytics_resource.py | 12 +- 28 files changed, 224 insertions(+), 2668 deletions(-) diff --git a/README.md b/README.md index 0d31354..5d0881d 100644 --- a/README.md +++ b/README.md @@ -120,20 +120,6 @@ MailerSend Python SDK - [Create an email verification list](#create-an-email-verification-list) - [Verify a list](#verify-a-list) - [Get list results](#get-list-results) - - [Webhooks](#webhooks-1) - - [Get a list of webhooks](#get-a-list-of-webhooks-1) - - [Get a single webhook](#get-a-single-webhook-1) - - [Create a Webhook](#create-a-webhook-1) - - [Create a disabled webhook](#create-a-disabled-webhook-1) - - [Update a Webhook](#update-a-webhook-1) - - [Disable/Enable a Webhook](#disableenable-a-webhook-1) - - [Delete a Webhook](#delete-a-webhook-1) - - [Email Verification](#email-verification-1) - - [Get all email verification lists](#get-all-email-verification-lists-1) - - [Get a single email verification list](#get-a-single-email-verification-list-1) - - [Create an email verification list](#create-an-email-verification-list-1) - - [Verify a list](#verify-a-list-1) - - [Get list results](#get-list-results-1) - [SMS](#sms) - [Sending SMS messages](#sending-sms-messages) - [SMS Activity](#sms-activity) @@ -1840,204 +1826,6 @@ request = (EmailVerificationBuilder() response = ms.email_verification.get_results(request) ``` -## Webhooks - -### Get a list of webhooks - -```python -from mailersend import MailerSendClient -from mailersend import WebhooksBuilder - -ms = MailerSendClient() - -request = (WebhooksBuilder() - .domain_id("domain-id") - .build_webhooks_list_request()) - -response = ms.webhooks.list_webhooks(request) -``` - -### Get a single webhook - -```python -from mailersend import MailerSendClient -from mailersend import WebhooksBuilder - -ms = MailerSendClient() - -request = (WebhooksBuilder() - .webhook_id("webhook-id") - .build_webhook_get_request()) - -response = ms.webhooks.get_webhook(request) -``` - -### Create a Webhook - -```python -from mailersend import MailerSendClient -from mailersend import WebhooksBuilder - -ms = MailerSendClient() - -request = (WebhooksBuilder() - .domain_id("domain-id") - .url("https://webhook.example.com") - .name("My Webhook") - .events(["activity.sent", "activity.delivered", "activity.opened"]) - .enabled(True) - .build_webhook_create_request()) - -response = ms.webhooks.create_webhook(request) -``` - -### Create a disabled webhook - -```python -from mailersend import MailerSendClient -from mailersend import WebhooksBuilder - -ms = MailerSendClient() - -request = (WebhooksBuilder() - .domain_id("domain-id") - .url("https://webhook.example.com") - .name("Disabled Webhook") - .events(["activity.sent", "activity.delivered"]) - .enabled(False) # Create disabled - .build_webhook_create_request()) - -response = ms.webhooks.create_webhook(request) -``` - -### Update a Webhook - -```python -from mailersend import MailerSendClient -from mailersend import WebhooksBuilder - -ms = MailerSendClient() - -request = (WebhooksBuilder() - .webhook_id("webhook-id") - .name("Updated Webhook Name") - .url("https://new-webhook.example.com") - .enabled(True) - .build_webhook_update_request()) - -response = ms.webhooks.update_webhook(request) -``` - -### Disable/Enable a Webhook - -```python -from mailersend import MailerSendClient -from mailersend import WebhooksBuilder - -ms = MailerSendClient() - -# Disable webhook -request = (WebhooksBuilder() - .webhook_id("webhook-id") - .enabled(False) - .build_webhook_update_request()) - -response = ms.webhooks.update_webhook(request) - -# Enable webhook -request = (WebhooksBuilder() - .webhook_id("webhook-id") - .enabled(True) - .build_webhook_update_request()) - -response = ms.webhooks.update_webhook(request) -``` - -### Delete a Webhook - -```python -from mailersend import MailerSendClient -from mailersend import WebhooksBuilder - -ms = MailerSendClient() - -request = (WebhooksBuilder() - .webhook_id("webhook-id") - .build_webhook_delete_request()) - -response = ms.webhooks.delete_webhook(request) -``` - -## Email Verification - -### Get all email verification lists - -```python -from mailersend import MailerSendClient, EmailVerificationBuilder - -ms = MailerSendClient() - -request = EmailVerificationBuilder().build_list_request() -response = ms.email_verification.list_verification_lists(request) -``` - -### Get a single email verification list - -```python -from mailersend import MailerSendClient, EmailVerificationBuilder - -ms = MailerSendClient() - -request = (EmailVerificationBuilder() - .verification_list_id("list-id") - .build_get_request()) - -response = ms.email_verification.get_verification_list(request) -``` - -### Create an email verification list - -```python -from mailersend import MailerSendClient, EmailVerificationBuilder - -ms = MailerSendClient() - -request = (EmailVerificationBuilder() - .name("My Verification List") - .emails(["test1@example.com", "test2@example.com"]) - .build_create_request()) - -response = ms.email_verification.create_verification_list(request) -``` - -### Verify a list - -```python -from mailersend import MailerSendClient, EmailVerificationBuilder - -ms = MailerSendClient() - -request = (EmailVerificationBuilder() - .verification_list_id("list-id") - .build_verify_request()) - -response = ms.email_verification.verify_list(request) -``` - -### Get list results - -```python -from mailersend import MailerSendClient, EmailVerificationBuilder - -ms = MailerSendClient() - -request = (EmailVerificationBuilder() - .verification_list_id("list-id") - .build_results_request()) - -response = ms.email_verification.get_verification_results(request) -``` - ## SMS ### Sending SMS messages @@ -2941,7 +2729,7 @@ from mailersend import AsyncMailerSendClient, DomainsBuilder, TemplatesBuilder async def main(): async with AsyncMailerSendClient() as client: domains_request = DomainsBuilder().build_list_request() - templates_request = TemplatesBuilder().build_list_request() + templates_request = TemplatesBuilder().build_templates_list_request() # Both requests run concurrently domains_response, templates_response = await asyncio.gather( @@ -3050,10 +2838,12 @@ except Exception as e: Common error types: - **ValidationError**: Invalid data in request models (handled by Pydantic) -- **AuthenticationError**: Invalid or missing API key -- **RateLimitError**: API rate limit exceeded -- **APIError**: General API errors (4xx, 5xx responses) -- **NetworkError**: Network connectivity issues +- **AuthenticationError**: Invalid or missing API key (401) +- **RateLimitExceeded**: API rate limit exceeded (429) +- **BadRequestError**: Malformed or invalid request (400) +- **ResourceNotFoundError**: Requested resource not found (404) +- **ServerError**: Server-side error (5xx) +- **MailerSendError**: Base exception; also raised for network connectivity failures # Testing diff --git a/mailersend/resources/activity.py b/mailersend/resources/activity.py index 5752ef1..1a5ce63 100644 --- a/mailersend/resources/activity.py +++ b/mailersend/resources/activity.py @@ -1,6 +1,6 @@ """Activity resource""" -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.activity import ActivityRequest, SingleActivityRequest from ..models.base import APIResponse @@ -28,12 +28,10 @@ def get(self, request: ActivityRequest) -> APIResponse: self.logger.debug("Getting activity data for domain: %s", request.domain_id) self.logger.debug("Query params: %s", params) - response = self.client.request( + return self._request( method="GET", path=f"activity/{request.domain_id}", params=params ) - return self._create_response(response) - def get_single(self, request: SingleActivityRequest) -> APIResponse: """ Get a single activity by its ID. @@ -47,45 +45,7 @@ def get_single(self, request: SingleActivityRequest) -> APIResponse: self.logger.debug("Preparing to get single activity") self.logger.debug("Getting single activity: %s", request.activity_id) - response = self.client.request( - method="GET", path=f"activities/{request.activity_id}" - ) - - return self._create_response(response) - + return self._request(method="GET", path=f"activities/{request.activity_id}") -class AsyncActivity(AsyncBaseResource): - """Async client for interacting with the MailerSend Activity API.""" - async def get(self, request: ActivityRequest) -> APIResponse: - """ - Get activity data for a domain. - - Args: - request: A fully-validated ActivityRequest object - - Returns: - APIResponse with activity data and metadata - """ - self.logger.debug("Preparing to get activity data") - params = request.to_query_params() - response = await self.client.request( - method="GET", path=f"activity/{request.domain_id}", params=params - ) - return self._create_response(response) - - async def get_single(self, request: SingleActivityRequest) -> APIResponse: - """ - Get a single activity by its ID. - - Args: - request: A fully-validated SingleActivityRequest object - - Returns: - APIResponse with single activity data - """ - self.logger.debug("Getting single activity: %s", request.activity_id) - response = await self.client.request( - method="GET", path=f"activities/{request.activity_id}" - ) - return self._create_response(response) +AsyncActivity = Activity diff --git a/mailersend/resources/analytics.py b/mailersend/resources/analytics.py index 716b4f3..ed93dde 100644 --- a/mailersend/resources/analytics.py +++ b/mailersend/resources/analytics.py @@ -2,7 +2,7 @@ from typing import Dict, Any, Optional -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.analytics import AnalyticsRequest from ..models.base import APIResponse @@ -33,9 +33,7 @@ def get_activity_by_date(self, request: AnalyticsRequest) -> APIResponse: self.logger.info("Requesting analytics data by date") self.logger.debug("Query params: %s", params) - response = self.client.request("GET", "analytics/date", params=params) - - return self._create_response(response) + return self._request("GET", "analytics/date", params=params) def get_opens_by_country(self, request: AnalyticsRequest) -> APIResponse: """ @@ -55,9 +53,7 @@ def get_opens_by_country(self, request: AnalyticsRequest) -> APIResponse: self.logger.info("Requesting analytics data by country") self.logger.debug("Query params: %s", params) - response = self.client.request("GET", "analytics/country", params=params) - - return self._create_response(response) + return self._request("GET", "analytics/country", params=params) def get_opens_by_user_agent(self, request: AnalyticsRequest) -> APIResponse: """ @@ -77,9 +73,7 @@ def get_opens_by_user_agent(self, request: AnalyticsRequest) -> APIResponse: self.logger.info("Requesting analytics data by user agent") self.logger.debug("Query params: %s", params) - response = self.client.request("GET", "analytics/ua-name", params=params) - - return self._create_response(response) + return self._request("GET", "analytics/ua-name", params=params) def get_opens_by_reading_environment( self, request: AnalyticsRequest @@ -101,9 +95,7 @@ def get_opens_by_reading_environment( self.logger.info("Requesting analytics data by reading environment") self.logger.debug("Query params: %s", params) - response = self.client.request("GET", "analytics/ua-type", params=params) - - return self._create_response(response) + return self._request("GET", "analytics/ua-type", params=params) def _build_query_params( self, request: AnalyticsRequest, exclude_fields: Optional[list] = None @@ -131,73 +123,4 @@ def _build_query_params( return params -class AsyncAnalytics(AsyncBaseResource): - """Async client for interacting with the MailerSend Analytics API.""" - - async def get_activity_by_date(self, request: AnalyticsRequest) -> APIResponse: - """ - Retrieve analytics data grouped by date. - - Args: - request: AnalyticsRequest with date range and filtering options - - Returns: - APIResponse with activity data grouped by date - """ - params = self._build_query_params(request) - response = await self.client.request("GET", "analytics/date", params=params) - return self._create_response(response) - - async def get_opens_by_country(self, request: AnalyticsRequest) -> APIResponse: - """ - Retrieve analytics data grouped by country. - - Args: - request: AnalyticsRequest with date range and filtering options - - Returns: - APIResponse with opens data grouped by country - """ - params = self._build_query_params(request, exclude_fields=["event", "group_by"]) - response = await self.client.request("GET", "analytics/country", params=params) - return self._create_response(response) - - async def get_opens_by_user_agent(self, request: AnalyticsRequest) -> APIResponse: - """ - Retrieve analytics data grouped by user agent name. - - Args: - request: AnalyticsRequest with date range and filtering options - - Returns: - APIResponse with opens data grouped by user agent - """ - params = self._build_query_params(request, exclude_fields=["event", "group_by"]) - response = await self.client.request("GET", "analytics/ua-name", params=params) - return self._create_response(response) - - async def get_opens_by_reading_environment( - self, request: AnalyticsRequest - ) -> APIResponse: - """ - Retrieve analytics data grouped by reading environment. - - Args: - request: AnalyticsRequest with date range and filtering options - - Returns: - APIResponse with opens data grouped by reading environment - """ - params = self._build_query_params(request, exclude_fields=["event", "group_by"]) - response = await self.client.request("GET", "analytics/ua-type", params=params) - return self._create_response(response) - - def _build_query_params( - self, request: AnalyticsRequest, exclude_fields: Optional[list] = None - ) -> Dict[str, Any]: - exclude_fields = exclude_fields or [] - params = request.model_dump(by_alias=True, exclude_none=True) - for field in exclude_fields: - params.pop(field, None) - params.pop(f"{field}[]", None) - return params +AsyncAnalytics = Analytics diff --git a/mailersend/resources/base.py b/mailersend/resources/base.py index ceecbfb..6d6d871 100644 --- a/mailersend/resources/base.py +++ b/mailersend/resources/base.py @@ -1,5 +1,6 @@ +import inspect import logging -from typing import Any, Dict, Optional, Union, List, TypeVar, Type, ClassVar +from typing import Any, Dict, Optional, Union, TypeVar, Type, ClassVar from ..models.base import BaseModel, ModelList, APIResponse from ..logging import get_logger @@ -50,6 +51,26 @@ def _create_response(self, response: Any, data: Any = None) -> APIResponse: ), ) + def _request(self, method, path, params=None, body=None, data=None) -> Any: + kwargs = {"method": method, "path": path} + if params is not None: + kwargs["params"] = params + if body is not None: + kwargs["body"] = body + result = self.client.request(**kwargs) + + if inspect.isawaitable(result): + async def resolve(): + response = await result + if data is not None: + return self._create_response(response, data(response)) + return self._create_response(response) + return resolve() + + if data is not None: + return self._create_response(result, data(result)) + return self._create_response(result) + def _parse_int_header(self, response: Any, header: str) -> Optional[int]: """ Safely parse integer header value. diff --git a/mailersend/resources/dmarc_monitoring.py b/mailersend/resources/dmarc_monitoring.py index 2b0a23f..0333d28 100644 --- a/mailersend/resources/dmarc_monitoring.py +++ b/mailersend/resources/dmarc_monitoring.py @@ -2,7 +2,7 @@ from typing import Optional -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.base import APIResponse from ..models.dmarc_monitoring import ( DmarcMonitoringListRequest, @@ -40,10 +40,7 @@ def list_monitors( params = request.to_query_params() self.logger.debug("Listing DMARC monitors with params: %s", params) - response = self.client.request( - method="GET", path="dmarc-monitoring", params=params - ) - return self._create_response(response) + return self._request(method="GET", path="dmarc-monitoring", params=params) def create_monitor(self, request: DmarcMonitoringCreateRequest) -> APIResponse: """ @@ -58,10 +55,7 @@ def create_monitor(self, request: DmarcMonitoringCreateRequest) -> APIResponse: body = request.model_dump(by_alias=True, exclude_none=True) self.logger.debug("Creating DMARC monitor with body: %s", body) - response = self.client.request( - method="POST", path="dmarc-monitoring", body=body - ) - return self._create_response(response) + return self._request(method="POST", path="dmarc-monitoring", body=body) def update_monitor(self, request: DmarcMonitoringUpdateRequest) -> APIResponse: """ @@ -80,10 +74,9 @@ def update_monitor(self, request: DmarcMonitoringUpdateRequest) -> APIResponse: "Updating DMARC monitor %s with body: %s", request.monitor_id, body ) - response = self.client.request( + return self._request( method="PUT", path=f"dmarc-monitoring/{request.monitor_id}", body=body ) - return self._create_response(response) def delete_monitor(self, request: DmarcMonitoringDeleteRequest) -> APIResponse: """ @@ -97,10 +90,9 @@ def delete_monitor(self, request: DmarcMonitoringDeleteRequest) -> APIResponse: """ self.logger.debug("Deleting DMARC monitor: %s", request.monitor_id) - response = self.client.request( + return self._request( method="DELETE", path=f"dmarc-monitoring/{request.monitor_id}" ) - return self._create_response(response) def get_aggregated_report( self, request: DmarcMonitoringReportRequest @@ -121,12 +113,11 @@ def get_aggregated_report( params, ) - response = self.client.request( + return self._request( method="GET", path=f"dmarc-monitoring/{request.monitor_id}/report", params=params, ) - return self._create_response(response) def get_ip_report(self, request: DmarcMonitoringIpReportRequest) -> APIResponse: """ @@ -146,12 +137,11 @@ def get_ip_report(self, request: DmarcMonitoringIpReportRequest) -> APIResponse: params, ) - response = self.client.request( + return self._request( method="GET", path=f"dmarc-monitoring/{request.monitor_id}/report/{request.ip}", params=params, ) - return self._create_response(response) def get_report_sources( self, request: DmarcMonitoringReportSourcesRequest @@ -167,11 +157,10 @@ def get_report_sources( """ self.logger.debug("Getting report sources for monitor: %s", request.monitor_id) - response = self.client.request( + return self._request( method="GET", path=f"dmarc-monitoring/{request.monitor_id}/report-sources", ) - return self._create_response(response) def mark_ip_favorite(self, request: DmarcMonitoringFavoriteRequest) -> APIResponse: """ @@ -187,11 +176,10 @@ def mark_ip_favorite(self, request: DmarcMonitoringFavoriteRequest) -> APIRespon "Marking IP %s as favorite for monitor: %s", request.ip, request.monitor_id ) - response = self.client.request( + return self._request( method="PUT", path=f"dmarc-monitoring/{request.monitor_id}/favorite/{request.ip}", ) - return self._create_response(response) def remove_ip_favorite( self, request: DmarcMonitoringFavoriteRequest @@ -211,182 +199,10 @@ def remove_ip_favorite( request.monitor_id, ) - response = self.client.request( + return self._request( method="DELETE", path=f"dmarc-monitoring/{request.monitor_id}/favorite/{request.ip}", ) - return self._create_response(response) - - -class AsyncDmarcMonitoring(AsyncBaseResource): - """Async client for the MailerSend DMARC Monitoring API.""" - - async def list_monitors( - self, request: Optional[DmarcMonitoringListRequest] = None - ) -> APIResponse: - """ - Retrieve a list of DMARC monitors. - - Args: - request: Optional DmarcMonitoringListRequest with pagination options - - Returns: - APIResponse with list of monitors - """ - if request is None: - request = DmarcMonitoringListRequest( - query_params=DmarcMonitoringListQueryParams() - ) - params = request.to_query_params() - response = await self.client.request( - method="GET", path="dmarc-monitoring", params=params - ) - return self._create_response(response) - - async def create_monitor( - self, request: DmarcMonitoringCreateRequest - ) -> APIResponse: - """ - Create a new DMARC monitor. - - Args: - request: DmarcMonitoringCreateRequest with domain_id - - Returns: - APIResponse with created monitor information - """ - body = request.model_dump(by_alias=True, exclude_none=True) - response = await self.client.request( - method="POST", path="dmarc-monitoring", body=body - ) - return self._create_response(response) - - async def update_monitor( - self, request: DmarcMonitoringUpdateRequest - ) -> APIResponse: - """ - Update a DMARC monitor. - - Args: - request: DmarcMonitoringUpdateRequest with monitor_id and wanted_dmarc_record - - Returns: - APIResponse with updated monitor information - """ - body = request.model_dump( - by_alias=True, exclude_none=True, exclude={"monitor_id"} - ) - response = await self.client.request( - method="PUT", path=f"dmarc-monitoring/{request.monitor_id}", body=body - ) - return self._create_response(response) - - async def delete_monitor( - self, request: DmarcMonitoringDeleteRequest - ) -> APIResponse: - """ - Delete a DMARC monitor. - - Args: - request: DmarcMonitoringDeleteRequest with monitor_id - - Returns: - APIResponse - """ - response = await self.client.request( - method="DELETE", path=f"dmarc-monitoring/{request.monitor_id}" - ) - return self._create_response(response) - - async def get_aggregated_report( - self, request: DmarcMonitoringReportRequest - ) -> APIResponse: - """ - Get aggregated DMARC reports for a monitor. - Args: - request: DmarcMonitoringReportRequest with monitor_id and pagination options - Returns: - APIResponse with aggregated report data - """ - params = request.to_query_params() - response = await self.client.request( - method="GET", - path=f"dmarc-monitoring/{request.monitor_id}/report", - params=params, - ) - return self._create_response(response) - - async def get_ip_report( - self, request: DmarcMonitoringIpReportRequest - ) -> APIResponse: - """ - Get IP-specific DMARC reports for a monitor. - - Args: - request: DmarcMonitoringIpReportRequest with monitor_id, ip, and pagination options - - Returns: - APIResponse with IP-specific report data - """ - params = request.to_query_params() - response = await self.client.request( - method="GET", - path=f"dmarc-monitoring/{request.monitor_id}/report/{request.ip}", - params=params, - ) - return self._create_response(response) - - async def get_report_sources( - self, request: DmarcMonitoringReportSourcesRequest - ) -> APIResponse: - """ - Get report sources for a DMARC monitor. - - Args: - request: DmarcMonitoringReportSourcesRequest with monitor_id - - Returns: - APIResponse with report sources data - """ - response = await self.client.request( - method="GET", path=f"dmarc-monitoring/{request.monitor_id}/report-sources" - ) - return self._create_response(response) - - async def mark_ip_favorite( - self, request: DmarcMonitoringFavoriteRequest - ) -> APIResponse: - """ - Mark an IP address as favorite for a DMARC monitor. - - Args: - request: DmarcMonitoringFavoriteRequest with monitor_id and ip - - Returns: - APIResponse - """ - response = await self.client.request( - method="PUT", - path=f"dmarc-monitoring/{request.monitor_id}/favorite/{request.ip}", - ) - return self._create_response(response) - - async def remove_ip_favorite( - self, request: DmarcMonitoringFavoriteRequest - ) -> APIResponse: - """ - Remove an IP address from favorites for a DMARC monitor. - - Args: - request: DmarcMonitoringFavoriteRequest with monitor_id and ip - - Returns: - APIResponse - """ - response = await self.client.request( - method="DELETE", - path=f"dmarc-monitoring/{request.monitor_id}/favorite/{request.ip}", - ) - return self._create_response(response) +AsyncDmarcMonitoring = DmarcMonitoring diff --git a/mailersend/resources/domains.py b/mailersend/resources/domains.py index 4ae2a35..23a4ee8 100644 --- a/mailersend/resources/domains.py +++ b/mailersend/resources/domains.py @@ -2,7 +2,7 @@ from typing import Optional -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.domains import ( DomainListRequest, DomainCreateRequest, @@ -46,9 +46,7 @@ def list_domains(self, request: Optional[DomainListRequest] = None) -> APIRespon self.logger.debug("Query params: %s", params) - response = self.client.request(method="GET", path="domains", params=params) - - return self._create_response(response) + return self._request(method="GET", path="domains", params=params) def get_domain(self, request: DomainGetRequest) -> APIResponse: """ @@ -63,11 +61,7 @@ def get_domain(self, request: DomainGetRequest) -> APIResponse: self.logger.debug("Preparing to get domain") self.logger.debug("Requesting domain information for: %s", request.domain_id) - response = self.client.request( - method="GET", path=f"domains/{request.domain_id}" - ) - - return self._create_response(response) + return self._request(method="GET", path=f"domains/{request.domain_id}") def create_domain(self, request: DomainCreateRequest) -> APIResponse: """ @@ -86,9 +80,7 @@ def create_domain(self, request: DomainCreateRequest) -> APIResponse: self.logger.debug("Request body: %s", body) - response = self.client.request(method="POST", path="domains", body=body) - - return self._create_response(response) + return self._request(method="POST", path="domains", body=body) def delete_domain(self, request: DomainDeleteRequest) -> APIResponse: """ @@ -103,11 +95,7 @@ def delete_domain(self, request: DomainDeleteRequest) -> APIResponse: self.logger.debug("Preparing to delete domain") self.logger.debug("Deleting domain: %s", request.domain_id) - response = self.client.request( - method="DELETE", path=f"domains/{request.domain_id}" - ) - - return self._create_response(response) + return self._request(method="DELETE", path=f"domains/{request.domain_id}") def get_domain_recipients(self, request: DomainRecipientsRequest) -> APIResponse: """ @@ -127,12 +115,12 @@ def get_domain_recipients(self, request: DomainRecipientsRequest) -> APIResponse self.logger.debug("Query params: %s", params) - response = self.client.request( - method="GET", path=f"domains/{request.domain_id}/recipients", params=params + return self._request( + method="GET", + path=f"domains/{request.domain_id}/recipients", + params=params, ) - return self._create_response(response) - def update_domain_settings( self, request: DomainUpdateSettingsRequest ) -> APIResponse: @@ -155,12 +143,10 @@ def update_domain_settings( self.logger.debug("Request body: %s", body) - response = self.client.request( + return self._request( method="PUT", path=f"domains/{request.domain_id}/settings", body=body ) - return self._create_response(response) - def get_domain_dns_records(self, request: DomainDnsRecordsRequest) -> APIResponse: """ Retrieve DNS records for a domain. @@ -174,12 +160,10 @@ def get_domain_dns_records(self, request: DomainDnsRecordsRequest) -> APIRespons self.logger.debug("Preparing to get domain DNS records") self.logger.debug("Retrieving DNS records for domain: %s", request.domain_id) - response = self.client.request( + return self._request( method="GET", path=f"domains/{request.domain_id}/dns-records" ) - return self._create_response(response) - def get_domain_verification_status( self, request: DomainVerificationRequest ) -> APIResponse: @@ -197,150 +181,9 @@ def get_domain_verification_status( "Retrieving verification status for domain: %s", request.domain_id ) - response = self.client.request( + return self._request( method="GET", path=f"domains/{request.domain_id}/verify" ) - return self._create_response(response) - - -class AsyncDomains(AsyncBaseResource): - """Async client for interacting with the MailerSend Domains API.""" - - async def list_domains( - self, request: Optional[DomainListRequest] = None - ) -> APIResponse: - """ - Retrieve a list of domains. - - Args: - request: Optional DomainListRequest with filtering and pagination options - Returns: - APIResponse with list of domains - """ - if not request: - query_params = DomainListQueryParams() - params = query_params.to_query_params() - else: - params = request.to_query_params() - response = await self.client.request( - method="GET", path="domains", params=params - ) - return self._create_response(response) - - async def get_domain(self, request: DomainGetRequest) -> APIResponse: - """ - Retrieve information about a single domain. - - Args: - request: DomainGetRequest with domain ID - - Returns: - APIResponse with domain information - """ - response = await self.client.request( - method="GET", path=f"domains/{request.domain_id}" - ) - return self._create_response(response) - - async def create_domain(self, request: DomainCreateRequest) -> APIResponse: - """ - Create a new domain. - - Args: - request: DomainCreateRequest with domain creation details - - Returns: - APIResponse with created domain information - """ - body = request.model_dump(by_alias=True, exclude_none=True) - response = await self.client.request(method="POST", path="domains", body=body) - return self._create_response(response) - - async def delete_domain(self, request: DomainDeleteRequest) -> APIResponse: - """ - Delete a domain. - - Args: - request: DomainDeleteRequest with domain ID to delete - - Returns: - APIResponse (204 No Content on success) - """ - response = await self.client.request( - method="DELETE", path=f"domains/{request.domain_id}" - ) - return self._create_response(response) - - async def get_domain_recipients( - self, request: DomainRecipientsRequest - ) -> APIResponse: - """ - Retrieve recipients for a domain. - - Args: - request: DomainRecipientsRequest with domain ID and pagination options - - Returns: - APIResponse with list of domain recipients - """ - params = request.to_query_params() - response = await self.client.request( - method="GET", path=f"domains/{request.domain_id}/recipients", params=params - ) - return self._create_response(response) - - async def update_domain_settings( - self, request: DomainUpdateSettingsRequest - ) -> APIResponse: - """ - Update domain settings. - - Args: - request: DomainUpdateSettingsRequest with domain ID and settings to update - - Returns: - APIResponse with updated domain information - """ - body = request.model_dump( - by_alias=True, exclude_none=True, exclude={"domain_id"} - ) - response = await self.client.request( - method="PUT", path=f"domains/{request.domain_id}/settings", body=body - ) - return self._create_response(response) - - async def get_domain_dns_records( - self, request: DomainDnsRecordsRequest - ) -> APIResponse: - """ - Retrieve DNS records for a domain. - - Args: - request: DomainDnsRecordsRequest with domain ID - - Returns: - APIResponse with domain DNS records - """ - response = await self.client.request( - method="GET", path=f"domains/{request.domain_id}/dns-records" - ) - return self._create_response(response) - - async def get_domain_verification_status( - self, request: DomainVerificationRequest - ) -> APIResponse: - """ - Retrieve verification status for a domain. - - Args: - request: DomainVerificationRequest with domain ID - - Returns: - APIResponse with domain verification status - """ - response = await self.client.request( - method="GET", path=f"domains/{request.domain_id}/verify" - ) - return self._create_response(response) +AsyncDomains = Domains diff --git a/mailersend/resources/email.py b/mailersend/resources/email.py index bd3a11c..949c614 100644 --- a/mailersend/resources/email.py +++ b/mailersend/resources/email.py @@ -2,7 +2,7 @@ from typing import List -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.email import EmailRequest from ..models.base import APIResponse @@ -30,12 +30,12 @@ def send(self, email: EmailRequest) -> APIResponse: self.logger.debug("Sending email request to MailerSend API") self.logger.debug("Payload: %s", payload) - response = self.client.request(method="POST", path="email", body=payload) - - # Create custom data with email ID from headers - email_data = {"id": response.headers.get("x-message-id")} - - return self._create_response(response, email_data) + return self._request( + method="POST", + path="email", + body=payload, + data=lambda r: {"id": r.headers.get("x-message-id")}, + ) def send_bulk(self, emails: List[EmailRequest]) -> APIResponse: """ @@ -58,9 +58,7 @@ def send_bulk(self, emails: List[EmailRequest]) -> APIResponse: self.logger.debug("Sending bulk email request to MailerSend API") self.logger.debug("Payload: %s", payload) - response = self.client.request(method="POST", path="bulk-email", body=payload) - - return self._create_response(response) + return self._request(method="POST", path="bulk-email", body=payload) def get_bulk_status(self, bulk_email_id: str) -> APIResponse: """ @@ -74,61 +72,7 @@ def get_bulk_status(self, bulk_email_id: str) -> APIResponse: """ self.logger.debug("Getting bulk email status") - response = self.client.request(method="GET", path=f"bulk-email/{bulk_email_id}") - - return self._create_response(response) - - -class AsyncEmail(AsyncBaseResource): - """Async client for interacting with the MailerSend Email API.""" - - async def send(self, email: EmailRequest) -> APIResponse: - """ - Send a single email. - - Args: - email: A fully-validated EmailRequest object + return self._request(method="GET", path=f"bulk-email/{bulk_email_id}") - Returns: - APIResponse with email ID and metadata - """ - self.logger.debug("Preparing to send email") - payload = email.model_dump(by_alias=True, exclude_none=True) - self.logger.debug("Sending email request to MailerSend API") - response = await self.client.request(method="POST", path="email", body=payload) - email_data = {"id": response.headers.get("x-message-id")} - return self._create_response(response, email_data) - - async def send_bulk(self, emails: List[EmailRequest]) -> APIResponse: - """ - Send multiple emails in one request. - Args: - emails: List of EmailRequest objects to send - - Returns: - APIResponse with bulk email information and metadata - """ - self.logger.debug("Preparing to send emails in bulk") - payload = [e.model_dump(by_alias=True, exclude_none=True) for e in emails] - self.logger.debug("Sending bulk email request to MailerSend API") - response = await self.client.request( - method="POST", path="bulk-email", body=payload - ) - return self._create_response(response) - - async def get_bulk_status(self, bulk_email_id: str) -> APIResponse: - """ - Get the status of a bulk email send request. - - Args: - bulk_email_id: The ID of the bulk email request - - Returns: - APIResponse with bulk email status and metadata - """ - self.logger.debug("Getting bulk email status") - response = await self.client.request( - method="GET", path=f"bulk-email/{bulk_email_id}" - ) - return self._create_response(response) +AsyncEmail = Email diff --git a/mailersend/resources/email_verification.py b/mailersend/resources/email_verification.py index 2a2b5ca..28f9585 100644 --- a/mailersend/resources/email_verification.py +++ b/mailersend/resources/email_verification.py @@ -1,6 +1,6 @@ """Email Verification resource""" -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.base import APIResponse from ..models.email_verification import ( EmailVerifyRequest, @@ -12,7 +12,6 @@ EmailVerificationVerifyRequest, EmailVerificationResultsRequest, ) -from ..exceptions import ValidationError class EmailVerification(BaseResource): @@ -35,13 +34,10 @@ def verify_email(self, request: EmailVerifyRequest) -> APIResponse: self.logger.debug("Verifying email address: %s", body) # Make API call - response = self.client.request( + return self._request( method="POST", path="email-verification/verify", body=body ) - # Create standardized response - return self._create_response(response) - def verify_email_async(self, request: EmailVerifyAsyncRequest) -> APIResponse: """Verify a single email address (asynchronous). @@ -60,13 +56,10 @@ def verify_email_async(self, request: EmailVerifyAsyncRequest) -> APIResponse: self.logger.debug("Starting async verification for email: %s", body) # Make API call - response = self.client.request( + return self._request( method="POST", path="email-verification/verify-async", body=body ) - # Create standardized response - return self._create_response(response) - def get_async_status( self, request: EmailVerificationAsyncStatusRequest ) -> APIResponse: @@ -87,14 +80,11 @@ def get_async_status( ) # Make API call - response = self.client.request( + return self._request( method="GET", path=f"email-verification/verify-async/{request.email_verification_id}", ) - # Create standardized response - return self._create_response(response) - def list_verifications(self, request: EmailVerificationListsRequest) -> APIResponse: """List all email verification lists. @@ -112,13 +102,10 @@ def list_verifications(self, request: EmailVerificationListsRequest) -> APIRespo self.logger.debug("Listing email verification lists with params: %s", params) # Make API call - response = self.client.request( + return self._request( method="GET", path="email-verification", params=params ) - # Create standardized response - return self._create_response(response) - def get_verification(self, request: EmailVerificationGetRequest) -> APIResponse: """Get a single email verification list. @@ -136,13 +123,10 @@ def get_verification(self, request: EmailVerificationGetRequest) -> APIResponse: ) # Make API call - response = self.client.request( + return self._request( method="GET", path=f"email-verification/{request.email_verification_id}" ) - # Create standardized response - return self._create_response(response) - def create_verification( self, request: EmailVerificationCreateRequest ) -> APIResponse: @@ -166,13 +150,10 @@ def create_verification( ) # Make API call - response = self.client.request( + return self._request( method="POST", path="email-verification", body=body ) - # Create standardized response - return self._create_response(response) - def verify_list(self, request: EmailVerificationVerifyRequest) -> APIResponse: """Start verification of an email verification list. @@ -188,14 +169,11 @@ def verify_list(self, request: EmailVerificationVerifyRequest) -> APIResponse: ) # Make API call - response = self.client.request( + return self._request( method="GET", path=f"email-verification/{request.email_verification_id}/verify", ) - # Create standardized response - return self._create_response(response) - def get_results(self, request: EmailVerificationResultsRequest) -> APIResponse: """Get verification results for an email verification list. @@ -217,146 +195,11 @@ def get_results(self, request: EmailVerificationResultsRequest) -> APIResponse: ) # Make API call - response = self.client.request( + return self._request( method="GET", path=f"email-verification/{request.email_verification_id}/results", params=params, ) - # Create standardized response - return self._create_response(response) - - -class AsyncEmailVerification(AsyncBaseResource): - """Async resource for managing email verification.""" - - async def verify_email(self, request: EmailVerifyRequest) -> APIResponse: - """Verify a single email address (synchronous). - - Args: - request: The email verification request data. - - Returns: - APIResponse with verification result - """ - body = request.model_dump(exclude_none=True) - response = await self.client.request( - method="POST", path="email-verification/verify", body=body - ) - return self._create_response(response) - - async def verify_email_async(self, request: EmailVerifyAsyncRequest) -> APIResponse: - """Verify a single email address (asynchronous). - - Args: - request: The async email verification request data. - - Returns: - APIResponse with verification result - """ - body = request.model_dump(exclude_none=True) - response = await self.client.request( - method="POST", path="email-verification/verify-async", body=body - ) - return self._create_response(response) - - async def get_async_status( - self, request: EmailVerificationAsyncStatusRequest - ) -> APIResponse: - """Get the status of an async email verification. - - Args: - request: The async status request data. - - Returns: - APIResponse with EmailVerificationAsyncStatusResponse data - """ - response = await self.client.request( - method="GET", - path=f"email-verification/verify-async/{request.email_verification_id}", - ) - return self._create_response(response) - - async def list_verifications( - self, request: EmailVerificationListsRequest - ) -> APIResponse: - """List all email verification lists. - - Args: - request: The list request data with pagination options. - - Returns: - APIResponse with list of email verification lists - """ - params = request.to_query_params() - response = await self.client.request( - method="GET", path="email-verification", params=params - ) - return self._create_response(response) - - async def get_verification( - self, request: EmailVerificationGetRequest - ) -> APIResponse: - """Get a single email verification list. - - Args: - request: The get verification request data. - - Returns: - APIResponse with email verification list data - """ - response = await self.client.request( - method="GET", path=f"email-verification/{request.email_verification_id}" - ) - return self._create_response(response) - - async def create_verification( - self, request: EmailVerificationCreateRequest - ) -> APIResponse: - """Create a new email verification list. - - Args: - request: The create verification request data. - - Returns: - APIResponse with email verification list data - """ - body = request.model_dump(exclude_none=True) - response = await self.client.request( - method="POST", path="email-verification", body=body - ) - return self._create_response(response) - - async def verify_list(self, request: EmailVerificationVerifyRequest) -> APIResponse: - """Start verification of an email verification list. - - Args: - request: The verify list request data. - - Returns: - APIResponse with verification result - """ - response = await self.client.request( - method="GET", - path=f"email-verification/{request.email_verification_id}/verify", - ) - return self._create_response(response) - - async def get_results( - self, request: EmailVerificationResultsRequest - ) -> APIResponse: - """Get verification results for an email verification list. - Args: - request: The results request data with optional filters. - - Returns: - APIResponse with verification results - """ - params = request.to_query_params() - response = await self.client.request( - method="GET", - path=f"email-verification/{request.email_verification_id}/results", - params=params, - ) - return self._create_response(response) +AsyncEmailVerification = EmailVerification diff --git a/mailersend/resources/identities.py b/mailersend/resources/identities.py index 0a28f9a..cc7c2eb 100644 --- a/mailersend/resources/identities.py +++ b/mailersend/resources/identities.py @@ -11,7 +11,7 @@ IdentityDeleteByEmailRequest, ) from ..models.base import APIResponse -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource class IdentitiesResource(BaseResource): @@ -37,12 +37,10 @@ def list_identities(self, request: IdentityListRequest) -> APIResponse: ) # Make API request - response = self.client.request( + return self._request( method="GET", path="identities", params=params if params else None ) - return self._create_response(response) - def create_identity(self, request: IdentityCreateRequest) -> APIResponse: """ Create a new sender identity. @@ -64,9 +62,7 @@ def create_identity(self, request: IdentityCreateRequest) -> APIResponse: ) # Make API request - response = self.client.request(method="POST", path="identities", body=data) - - return self._create_response(response) + return self._request(method="POST", path="identities", body=data) def get_identity(self, request: IdentityGetRequest) -> APIResponse: """ @@ -81,12 +77,10 @@ def get_identity(self, request: IdentityGetRequest) -> APIResponse: self.logger.debug("Preparing to get identity with ID: %s", request.identity_id) # Make API request - response = self.client.request( + return self._request( method="GET", path=f"identities/{request.identity_id}" ) - return self._create_response(response) - def get_identity_by_email(self, request: IdentityGetByEmailRequest) -> APIResponse: """ Get a single sender identity by email. @@ -100,12 +94,10 @@ def get_identity_by_email(self, request: IdentityGetByEmailRequest) -> APIRespon self.logger.debug("Preparing to get identity by email: %s", request.email) # Make API request - response = self.client.request( + return self._request( method="GET", path=f"identities/email/{request.email}" ) - return self._create_response(response) - def update_identity(self, request: IdentityUpdateRequest) -> APIResponse: """ Update a sender identity by ID. @@ -131,14 +123,12 @@ def update_identity(self, request: IdentityUpdateRequest) -> APIResponse: ) # Make API request - response = self.client.request( + return self._request( method="PUT", path=f"identities/{request.identity_id}", body=data if data else None, ) - return self._create_response(response) - def update_identity_by_email( self, request: IdentityUpdateByEmailRequest ) -> APIResponse: @@ -162,14 +152,12 @@ def update_identity_by_email( ) # Make API request - response = self.client.request( + return self._request( method="PUT", path=f"identities/email/{request.email}", body=data if data else None, ) - return self._create_response(response) - def delete_identity(self, request: IdentityDeleteRequest) -> APIResponse: """ Delete a sender identity by ID. @@ -185,12 +173,10 @@ def delete_identity(self, request: IdentityDeleteRequest) -> APIResponse: ) # Make API request - response = self.client.request( + return self._request( method="DELETE", path=f"identities/{request.identity_id}" ) - return self._create_response(response) - def delete_identity_by_email( self, request: IdentityDeleteByEmailRequest ) -> APIResponse: @@ -206,148 +192,9 @@ def delete_identity_by_email( self.logger.debug("Preparing to delete identity by email: %s", request.email) # Make API request - response = self.client.request( + return self._request( method="DELETE", path=f"identities/email/{request.email}" ) - return self._create_response(response) - - -class AsyncIdentitiesResource(AsyncBaseResource): - """Async resource for managing sender identities.""" - - async def list_identities(self, request: IdentityListRequest) -> APIResponse: - """ - Get a list of sender identities. - - Args: - request: The identity list request containing filtering and pagination parameters - - Returns: - APIResponse containing the identities list response - """ - params = request.to_query_params() - response = await self.client.request( - method="GET", path="identities", params=params if params else None - ) - return self._create_response(response) - - async def create_identity(self, request: IdentityCreateRequest) -> APIResponse: - """ - Create a new sender identity. - - Args: - request: The identity creation request with all required data - - Returns: - APIResponse containing the created identity response - """ - data = request.model_dump(by_alias=True, exclude_none=True) - response = await self.client.request( - method="POST", path="identities", body=data - ) - return self._create_response(response) - - async def get_identity(self, request: IdentityGetRequest) -> APIResponse: - """ - Get a single sender identity by ID. - - Args: - request: The identity get request with identity ID - - Returns: - APIResponse containing the identity data - """ - response = await self.client.request( - method="GET", path=f"identities/{request.identity_id}" - ) - return self._create_response(response) - - async def get_identity_by_email( - self, request: IdentityGetByEmailRequest - ) -> APIResponse: - """ - Get a single sender identity by email. - - Args: - request: The identity get by email request - - Returns: - APIResponse containing the identity data - """ - response = await self.client.request( - method="GET", path=f"identities/email/{request.email}" - ) - return self._create_response(response) - - async def update_identity(self, request: IdentityUpdateRequest) -> APIResponse: - """ - Update a sender identity by ID. - - Args: - request: The identity update request with identity ID and update data - - Returns: - APIResponse containing the updated identity - """ - data = request.model_dump( - by_alias=True, exclude_none=True, exclude={"identity_id"} - ) - response = await self.client.request( - method="PUT", - path=f"identities/{request.identity_id}", - body=data if data else None, - ) - return self._create_response(response) - - async def update_identity_by_email( - self, request: IdentityUpdateByEmailRequest - ) -> APIResponse: - """ - Update a sender identity by email. - Args: - request: The identity update by email request - - Returns: - APIResponse containing the updated identity - """ - data = request.model_dump(by_alias=True, exclude_none=True, exclude={"email"}) - response = await self.client.request( - method="PUT", - path=f"identities/email/{request.email}", - body=data if data else None, - ) - return self._create_response(response) - - async def delete_identity(self, request: IdentityDeleteRequest) -> APIResponse: - """ - Delete a sender identity by ID. - - Args: - request: The identity delete request with identity ID - - Returns: - APIResponse containing the deletion result - """ - response = await self.client.request( - method="DELETE", path=f"identities/{request.identity_id}" - ) - return self._create_response(response) - - async def delete_identity_by_email( - self, request: IdentityDeleteByEmailRequest - ) -> APIResponse: - """ - Delete a sender identity by email. - - Args: - request: The identity delete by email request - - Returns: - APIResponse containing the deletion result - """ - response = await self.client.request( - method="DELETE", path=f"identities/email/{request.email}" - ) - return self._create_response(response) +AsyncIdentitiesResource = IdentitiesResource diff --git a/mailersend/resources/inbound.py b/mailersend/resources/inbound.py index c0d94cd..5e6f4fb 100644 --- a/mailersend/resources/inbound.py +++ b/mailersend/resources/inbound.py @@ -8,7 +8,7 @@ InboundDeleteRequest, ) from mailersend.models.base import APIResponse -from mailersend.resources.base import AsyncBaseResource, BaseResource +from mailersend.resources.base import BaseResource class InboundResource(BaseResource): @@ -34,12 +34,10 @@ def list(self, request: InboundListRequest) -> APIResponse: ) # Make API request - response = self.client.request( + return self._request( method="GET", path="inbound", params=params if params else None ) - return self._create_response(response) - def get(self, request: InboundGetRequest) -> APIResponse: """ Get a single inbound route by ID. @@ -55,11 +53,7 @@ def get(self, request: InboundGetRequest) -> APIResponse: ) # Make API request - response = self.client.request( - method="GET", path=f"inbound/{request.inbound_id}" - ) - - return self._create_response(response) + return self._request(method="GET", path=f"inbound/{request.inbound_id}") def create(self, request: InboundCreateRequest) -> APIResponse: """ @@ -82,9 +76,7 @@ def create(self, request: InboundCreateRequest) -> APIResponse: ) # Make API request - response = self.client.request(method="POST", path="inbound", body=data) - - return self._create_response(response) + return self._request(method="POST", path="inbound", body=data) def update(self, request: InboundUpdateRequest) -> APIResponse: """ @@ -109,12 +101,10 @@ def update(self, request: InboundUpdateRequest) -> APIResponse: ) # Make API request - response = self.client.request( + return self._request( method="PUT", path=f"inbound/{request.inbound_id}", body=data ) - return self._create_response(response) - def delete(self, request: InboundDeleteRequest) -> APIResponse: """ Delete an inbound route. @@ -130,88 +120,7 @@ def delete(self, request: InboundDeleteRequest) -> APIResponse: ) # Make API request - response = self.client.request( - method="DELETE", path=f"inbound/{request.inbound_id}" - ) - - return self._create_response(response) - - -class AsyncInboundResource(AsyncBaseResource): - """Async resource for managing inbound routes.""" - - async def list(self, request: InboundListRequest) -> APIResponse: - """ - Get a list of inbound routes. - - Args: - request: The inbound list request containing filtering and pagination parameters - - Returns: - APIResponse containing the inbound routes list response - """ - params = request.to_query_params() - response = await self.client.request( - method="GET", path="inbound", params=params if params else None - ) - return self._create_response(response) - - async def get(self, request: InboundGetRequest) -> APIResponse: - """ - Get a single inbound route by ID. - - Args: - request: The inbound get request with inbound ID - - Returns: - APIResponse containing the inbound route data - """ - response = await self.client.request( - method="GET", path=f"inbound/{request.inbound_id}" - ) - return self._create_response(response) + return self._request(method="DELETE", path=f"inbound/{request.inbound_id}") - async def create(self, request: InboundCreateRequest) -> APIResponse: - """ - Create a new inbound route. - - Args: - request: The inbound create request with all required data - Returns: - APIResponse containing the created inbound route response - """ - data = request.to_request_body() - response = await self.client.request(method="POST", path="inbound", body=data) - return self._create_response(response) - - async def update(self, request: InboundUpdateRequest) -> APIResponse: - """ - Update an existing inbound route. - - Args: - request: The inbound update request with inbound ID and update data - - Returns: - APIResponse containing the updated response - """ - data = request.to_request_body() - response = await self.client.request( - method="PUT", path=f"inbound/{request.inbound_id}", body=data - ) - return self._create_response(response) - - async def delete(self, request: InboundDeleteRequest) -> APIResponse: - """ - Delete an inbound route. - - Args: - request: The inbound delete request with inbound ID - - Returns: - APIResponse containing the deletion result - """ - response = await self.client.request( - method="DELETE", path=f"inbound/{request.inbound_id}" - ) - return self._create_response(response) +AsyncInboundResource = InboundResource diff --git a/mailersend/resources/messages.py b/mailersend/resources/messages.py index b69aab3..a790ad2 100644 --- a/mailersend/resources/messages.py +++ b/mailersend/resources/messages.py @@ -1,6 +1,6 @@ """Messages resource""" -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.messages import ( MessagesListRequest, MessageGetRequest, @@ -33,12 +33,10 @@ def list_messages(self, request: MessagesListRequest) -> APIResponse: self.logger.debug("Making API request to list messages with params: %s", params) # Make API request - response = self.client.request( + return self._request( method="GET", path="messages", params=params if params else None ) - return self._create_response(response) - def get_message(self, request: MessageGetRequest) -> APIResponse: """ Retrieve information about a single message. @@ -52,43 +50,7 @@ def get_message(self, request: MessageGetRequest) -> APIResponse: self.logger.debug("Preparing to get message with ID: %s", request.message_id) # Make API request - response = self.client.request( - method="GET", path=f"messages/{request.message_id}" - ) - - return self._create_response(response) - - -class AsyncMessages(AsyncBaseResource): - """Async client for interacting with the MailerSend Messages API.""" - - async def list_messages(self, request: MessagesListRequest) -> APIResponse: - """ - Retrieve a list of messages. + return self._request(method="GET", path=f"messages/{request.message_id}") - Args: - request: MessagesListRequest with pagination options - Returns: - APIResponse containing the messages list response - """ - params = request.to_query_params() - response = await self.client.request( - method="GET", path="messages", params=params if params else None - ) - return self._create_response(response) - - async def get_message(self, request: MessageGetRequest) -> APIResponse: - """ - Retrieve information about a single message. - - Args: - request: MessageGetRequest with message ID - - Returns: - APIResponse containing the message response - """ - response = await self.client.request( - method="GET", path=f"messages/{request.message_id}" - ) - return self._create_response(response) +AsyncMessages = Messages diff --git a/mailersend/resources/other.py b/mailersend/resources/other.py index 587d8fd..31bcc41 100644 --- a/mailersend/resources/other.py +++ b/mailersend/resources/other.py @@ -1,6 +1,6 @@ """Other endpoints resource""" -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.base import APIResponse @@ -20,20 +20,7 @@ def get_quota(self) -> APIResponse: """ self.logger.debug("Retrieving API quota information") - response = self.client.request(method="GET", path="api-quota") + return self._request(method="GET", path="api-quota") - return self._create_response(response) - -class AsyncOther(AsyncBaseResource): - """Async client for other MailerSend API endpoints.""" - - async def get_quota(self) -> APIResponse: - """ - Get API quota information. - - Returns: - APIResponse with quota information including remaining requests - """ - response = await self.client.request(method="GET", path="api-quota") - return self._create_response(response) +AsyncOther = Other diff --git a/mailersend/resources/recipients.py b/mailersend/resources/recipients.py index f0d6a38..e122165 100644 --- a/mailersend/resources/recipients.py +++ b/mailersend/resources/recipients.py @@ -13,7 +13,7 @@ RecipientsListQueryParams, SuppressionListQueryParams, ) -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource class Recipients(BaseResource): @@ -31,7 +31,6 @@ def list_recipients( Returns: APIResponse with recipients list """ - # Use default request if none provided if request is None: query_params = RecipientsListQueryParams() @@ -43,9 +42,7 @@ def list_recipients( self.logger.debug("Listing recipients with params: %s", params) # Make API call - response = self.client.request(method="GET", path="recipients", params=params) - - return self._create_response(response) + return self._request(method="GET", path="recipients", params=params) def get_recipient(self, request: RecipientGetRequest) -> APIResponse: """ @@ -57,12 +54,10 @@ def get_recipient(self, request: RecipientGetRequest) -> APIResponse: self.logger.debug("Getting recipient: %s", request.recipient_id) # Make API call - response = self.client.request( + return self._request( method="GET", path=f"recipients/{request.recipient_id}" ) - return self._create_response(response) - def delete_recipient(self, request: RecipientDeleteRequest) -> APIResponse: """ Delete a recipient. @@ -76,12 +71,10 @@ def delete_recipient(self, request: RecipientDeleteRequest) -> APIResponse: self.logger.debug("Deleting recipient: %s", request.recipient_id) # Make API call - response = self.client.request( + return self._request( method="DELETE", path=f"recipients/{request.recipient_id}" ) - return self._create_response(response) - def list_blocklist( self, request: Optional[SuppressionListRequest] = None ) -> APIResponse: @@ -105,12 +98,10 @@ def list_blocklist( self.logger.debug("Listing blocklist with params: %s", params) # Make API call - response = self.client.request( + return self._request( method="GET", path="suppressions/blocklist", params=params ) - return self._create_response(response) - def list_hard_bounces( self, request: Optional[SuppressionListRequest] = None ) -> APIResponse: @@ -134,12 +125,10 @@ def list_hard_bounces( self.logger.debug("Listing hard bounces with params: %s", params) # Make API call - response = self.client.request( + return self._request( method="GET", path="suppressions/hard-bounces", params=params ) - return self._create_response(response) - def list_spam_complaints( self, request: Optional[SuppressionListRequest] = None ) -> APIResponse: @@ -163,12 +152,10 @@ def list_spam_complaints( self.logger.debug("Listing spam complaints with params: %s", params) # Make API call - response = self.client.request( + return self._request( method="GET", path="suppressions/spam-complaints", params=params ) - return self._create_response(response) - def list_unsubscribes( self, request: Optional[SuppressionListRequest] = None ) -> APIResponse: @@ -192,12 +179,10 @@ def list_unsubscribes( self.logger.debug("Listing unsubscribes with params: %s", params) # Make API call - response = self.client.request( + return self._request( method="GET", path="suppressions/unsubscribes", params=params ) - return self._create_response(response) - def list_on_hold( self, request: Optional[SuppressionListRequest] = None ) -> APIResponse: @@ -221,12 +206,10 @@ def list_on_hold( self.logger.debug("Listing on-hold entries with params: %s", params) # Make API call - response = self.client.request( + return self._request( method="GET", path="suppressions/on-hold-list", params=params ) - return self._create_response(response) - def add_to_blocklist(self, request: SuppressionAddRequest) -> APIResponse: """ Add entries to blocklist. @@ -242,11 +225,7 @@ def add_to_blocklist(self, request: SuppressionAddRequest) -> APIResponse: self.logger.debug("Adding to blocklist with body: %s", body) # Make API call - response = self.client.request( - method="POST", path="suppressions/blocklist", body=body - ) - - return self._create_response(response) + return self._request(method="POST", path="suppressions/blocklist", body=body) def add_hard_bounces(self, request: SuppressionAddRequest) -> APIResponse: """ @@ -264,12 +243,10 @@ def add_hard_bounces(self, request: SuppressionAddRequest) -> APIResponse: self.logger.debug("Adding hard bounces with body: %s", body) # Make API call - response = self.client.request( + return self._request( method="POST", path="suppressions/hard-bounces", body=body ) - return self._create_response(response) - def add_spam_complaints(self, request: SuppressionAddRequest) -> APIResponse: """ Add spam complaints. @@ -287,12 +264,10 @@ def add_spam_complaints(self, request: SuppressionAddRequest) -> APIResponse: self.logger.debug("Adding spam complaints with body: %s", body) # Make API call - response = self.client.request( + return self._request( method="POST", path="suppressions/spam-complaints", body=body ) - return self._create_response(response) - def add_unsubscribes(self, request: SuppressionAddRequest) -> APIResponse: """ Add unsubscribes. @@ -309,12 +284,10 @@ def add_unsubscribes(self, request: SuppressionAddRequest) -> APIResponse: self.logger.debug("Adding unsubscribes with body: %s", body) # Make API call - response = self.client.request( + return self._request( method="POST", path="suppressions/unsubscribes", body=body ) - return self._create_response(response) - def delete_from_blocklist(self, request: SuppressionDeleteRequest) -> APIResponse: """ Delete entries from blocklist. @@ -331,12 +304,10 @@ def delete_from_blocklist(self, request: SuppressionDeleteRequest) -> APIRespons self.logger.debug("Deleting from blocklist with body: %s", body) # Make API call - response = self.client.request( + return self._request( method="DELETE", path="suppressions/blocklist", body=body ) - return self._create_response(response) - def delete_hard_bounces(self, request: SuppressionDeleteRequest) -> APIResponse: """ Delete hard bounces. @@ -353,12 +324,10 @@ def delete_hard_bounces(self, request: SuppressionDeleteRequest) -> APIResponse: self.logger.debug("Deleting hard bounces with body: %s", body) # Make API call - response = self.client.request( + return self._request( method="DELETE", path="suppressions/hard-bounces", body=body ) - return self._create_response(response) - def delete_spam_complaints(self, request: SuppressionDeleteRequest) -> APIResponse: """ Delete spam complaints. @@ -376,12 +345,10 @@ def delete_spam_complaints(self, request: SuppressionDeleteRequest) -> APIRespon self.logger.debug("Deleting spam complaints with body: %s", body) # Make API call - response = self.client.request( + return self._request( method="DELETE", path="suppressions/spam-complaints", body=body ) - return self._create_response(response) - def delete_unsubscribes(self, request: SuppressionDeleteRequest) -> APIResponse: """ Delete unsubscribes. @@ -398,12 +365,10 @@ def delete_unsubscribes(self, request: SuppressionDeleteRequest) -> APIResponse: self.logger.debug("Deleting unsubscribes with body: %s", body) # Make API call - response = self.client.request( + return self._request( method="DELETE", path="suppressions/unsubscribes", body=body ) - return self._create_response(response) - def delete_from_on_hold(self, request: SuppressionDeleteRequest) -> APIResponse: """ Delete entries from on-hold list. @@ -420,316 +385,9 @@ def delete_from_on_hold(self, request: SuppressionDeleteRequest) -> APIResponse: self.logger.debug("Deleting from on-hold with body: %s", body) # Make API call - response = self.client.request( + return self._request( method="DELETE", path="suppressions/on-hold-list", body=body ) - return self._create_response(response) - - -class AsyncRecipients(AsyncBaseResource): - """Async Recipients API resource.""" - - async def list_recipients( - self, request: Optional[RecipientsListRequest] = None - ) -> APIResponse: - """ - List recipients with optional filtering. - - Args: - request: Request parameters for listing recipients (optional) - - Returns: - APIResponse with recipients list - """ - if request is None: - request = RecipientsListRequest(query_params=RecipientsListQueryParams()) - params = request.to_query_params() - response = await self.client.request( - method="GET", path="recipients", params=params - ) - return self._create_response(response) - - async def get_recipient(self, request: RecipientGetRequest) -> APIResponse: - """ - Get a single recipient by ID. - - Args: - request: Request parameters for getting recipient - - Returns: - APIResponse with recipient data - """ - response = await self.client.request( - method="GET", path=f"recipients/{request.recipient_id}" - ) - return self._create_response(response) - - async def delete_recipient(self, request: RecipientDeleteRequest) -> APIResponse: - """ - Delete a recipient. - - Args: - request: Request parameters for deleting recipient - - Returns: - APIResponse with empty data - """ - response = await self.client.request( - method="DELETE", path=f"recipients/{request.recipient_id}" - ) - return self._create_response(response) - - async def list_blocklist( - self, request: Optional[SuppressionListRequest] = None - ) -> APIResponse: - """ - List blocklist entries. - - Args: - request: Request parameters for listing blocklist entries (optional) - - Returns: - APIResponse with blocklist entries - """ - if request is None: - request = SuppressionListRequest(query_params=SuppressionListQueryParams()) - params = request.to_query_params() - response = await self.client.request( - method="GET", path="suppressions/blocklist", params=params - ) - return self._create_response(response) - - async def list_hard_bounces( - self, request: Optional[SuppressionListRequest] = None - ) -> APIResponse: - """ - List hard bounces. - - Args: - request: Request parameters for listing hard bounces (optional) - - Returns: - APIResponse with hard bounces - """ - if request is None: - request = SuppressionListRequest(query_params=SuppressionListQueryParams()) - params = request.to_query_params() - response = await self.client.request( - method="GET", path="suppressions/hard-bounces", params=params - ) - return self._create_response(response) - - async def list_spam_complaints( - self, request: Optional[SuppressionListRequest] = None - ) -> APIResponse: - """ - List spam complaints. - - Args: - request: Request parameters for listing spam complaints (optional) - - Returns: - APIResponse with spam complaints - """ - if request is None: - request = SuppressionListRequest(query_params=SuppressionListQueryParams()) - params = request.to_query_params() - response = await self.client.request( - method="GET", path="suppressions/spam-complaints", params=params - ) - return self._create_response(response) - - async def list_unsubscribes( - self, request: Optional[SuppressionListRequest] = None - ) -> APIResponse: - """ - List unsubscribes. - - Args: - request: Request parameters for listing unsubscribes (optional) - - Returns: - APIResponse with unsubscribes - """ - if request is None: - request = SuppressionListRequest(query_params=SuppressionListQueryParams()) - params = request.to_query_params() - response = await self.client.request( - method="GET", path="suppressions/unsubscribes", params=params - ) - return self._create_response(response) - - async def list_on_hold( - self, request: Optional[SuppressionListRequest] = None - ) -> APIResponse: - """ - List on-hold entries. - - Args: - request: Request parameters for listing on-hold entries (optional) - - Returns: - APIResponse with on-hold entries - """ - if request is None: - request = SuppressionListRequest(query_params=SuppressionListQueryParams()) - params = request.to_query_params() - response = await self.client.request( - method="GET", path="suppressions/on-hold-list", params=params - ) - return self._create_response(response) - - async def add_to_blocklist(self, request: SuppressionAddRequest) -> APIResponse: - """ - Add entries to blocklist. - - Args: - request: Request parameters for adding to blocklist - Returns: - APIResponse with added entries - """ - body = request.model_dump(by_alias=True, exclude_none=True) - response = await self.client.request( - method="POST", path="suppressions/blocklist", body=body - ) - return self._create_response(response) - - async def add_hard_bounces(self, request: SuppressionAddRequest) -> APIResponse: - """ - Add hard bounces. - - Args: - request: Request parameters for adding hard bounces - - Returns: - APIResponse with added entries - """ - body = request.model_dump(by_alias=True, exclude_none=True) - response = await self.client.request( - method="POST", path="suppressions/hard-bounces", body=body - ) - return self._create_response(response) - - async def add_spam_complaints(self, request: SuppressionAddRequest) -> APIResponse: - """ - Add spam complaints. - - Args: - request: Request parameters for adding spam complaints - - Returns: - APIResponse with added entries - """ - body = request.model_dump(by_alias=True, exclude_none=True) - response = await self.client.request( - method="POST", path="suppressions/spam-complaints", body=body - ) - return self._create_response(response) - - async def add_unsubscribes(self, request: SuppressionAddRequest) -> APIResponse: - """ - Add unsubscribes. - - Args: - request: Request parameters for adding unsubscribes - - Returns: - APIResponse with added entries - """ - body = request.model_dump(by_alias=True, exclude_none=True) - response = await self.client.request( - method="POST", path="suppressions/unsubscribes", body=body - ) - return self._create_response(response) - - async def delete_from_blocklist( - self, request: SuppressionDeleteRequest - ) -> APIResponse: - """ - Delete entries from blocklist. - - Args: - request: Request parameters for deleting from blocklist - - Returns: - APIResponse with deleted entries - """ - body = request.model_dump(by_alias=True, exclude_none=True) - response = await self.client.request( - method="DELETE", path="suppressions/blocklist", body=body - ) - return self._create_response(response) - - async def delete_hard_bounces( - self, request: SuppressionDeleteRequest - ) -> APIResponse: - """ - Delete hard bounces. - - Args: - request: Request parameters for deleting hard bounces - - Returns: - APIResponse with deleted entries - """ - body = request.model_dump(exclude_none=True, exclude={"domain_id"}) - response = await self.client.request( - method="DELETE", path="suppressions/hard-bounces", body=body - ) - return self._create_response(response) - - async def delete_spam_complaints( - self, request: SuppressionDeleteRequest - ) -> APIResponse: - """ - Delete spam complaints. - - Args: - request: Request parameters for deleting spam complaints - - Returns: - APIResponse with deleted entries - """ - body = request.model_dump(exclude_none=True, exclude={"domain_id"}) - response = await self.client.request( - method="DELETE", path="suppressions/spam-complaints", body=body - ) - return self._create_response(response) - - async def delete_unsubscribes( - self, request: SuppressionDeleteRequest - ) -> APIResponse: - """ - Delete unsubscribes. - - Args: - request: Request parameters for deleting unsubscribes - - Returns: - APIResponse with deleted entries - """ - body = request.model_dump(exclude_none=True, exclude={"domain_id"}) - response = await self.client.request( - method="DELETE", path="suppressions/unsubscribes", body=body - ) - return self._create_response(response) - - async def delete_from_on_hold( - self, request: SuppressionDeleteRequest - ) -> APIResponse: - """ - Delete entries from on-hold list. - - Args: - request: Request parameters for deleting from on-hold - - Returns: - APIResponse with deleted entries - """ - body = request.model_dump(exclude_none=True, exclude={"domain_id"}) - response = await self.client.request( - method="DELETE", path="suppressions/on-hold-list", body=body - ) - return self._create_response(response) +AsyncRecipients = Recipients diff --git a/mailersend/resources/schedules.py b/mailersend/resources/schedules.py index 90815e6..8dd0544 100644 --- a/mailersend/resources/schedules.py +++ b/mailersend/resources/schedules.py @@ -1,6 +1,6 @@ """Schedules resource""" -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.schedules import ( SchedulesListRequest, ScheduleGetRequest, @@ -36,14 +36,12 @@ def list_schedules(self, request: SchedulesListRequest) -> APIResponse: ) # Make API request - response = self.client.request( + return self._request( method="GET", path="message-schedules", params=params if params else None, ) - return self._create_response(response) - def get_schedule(self, request: ScheduleGetRequest) -> APIResponse: """ Retrieve information about a single scheduled message. @@ -59,12 +57,10 @@ def get_schedule(self, request: ScheduleGetRequest) -> APIResponse: ) # Make API request - response = self.client.request( + return self._request( method="GET", path=f"message-schedules/{request.message_id}" ) - return self._create_response(response) - def delete_schedule(self, request: ScheduleDeleteRequest) -> APIResponse: """ Delete a scheduled message. @@ -80,58 +76,9 @@ def delete_schedule(self, request: ScheduleDeleteRequest) -> APIResponse: ) # Make API request - response = self.client.request( + return self._request( method="DELETE", path=f"message-schedules/{request.message_id}" ) - return self._create_response(response) - - -class AsyncSchedules(AsyncBaseResource): - """Async client for interacting with the MailerSend Schedules API.""" - - async def list_schedules(self, request: SchedulesListRequest) -> APIResponse: - """ - Retrieve a list of scheduled messages. - - Args: - request: SchedulesListRequest with filtering and pagination options - - Returns: - APIResponse containing the schedules list response - """ - params = request.to_query_params() - response = await self.client.request( - method="GET", path="message-schedules", params=params if params else None - ) - return self._create_response(response) - - async def get_schedule(self, request: ScheduleGetRequest) -> APIResponse: - """ - Retrieve information about a single scheduled message. - Args: - request: ScheduleGetRequest with message ID - - Returns: - APIResponse containing the schedule response - """ - response = await self.client.request( - method="GET", path=f"message-schedules/{request.message_id}" - ) - return self._create_response(response) - - async def delete_schedule(self, request: ScheduleDeleteRequest) -> APIResponse: - """ - Delete a scheduled message. - - Args: - request: ScheduleDeleteRequest with message ID to delete - - Returns: - APIResponse (204 No Content on success) - """ - response = await self.client.request( - method="DELETE", path=f"message-schedules/{request.message_id}" - ) - return self._create_response(response) +AsyncSchedules = Schedules diff --git a/mailersend/resources/sms_activity.py b/mailersend/resources/sms_activity.py index cb18980..245b009 100644 --- a/mailersend/resources/sms_activity.py +++ b/mailersend/resources/sms_activity.py @@ -2,7 +2,7 @@ SMS Activity API resource. """ -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.sms_activity import SmsActivityListRequest, SmsMessageGetRequest from ..models.base import APIResponse @@ -31,9 +31,7 @@ def list(self, request: SmsActivityListRequest) -> APIResponse: self.logger.debug("Listing SMS activities with params: %s", params) # Make API request - response = self.client.request(method="GET", path="sms-activity", params=params) - - return self._create_response(response) + return self._request(method="GET", path="sms-activity", params=params) def get(self, request: SmsMessageGetRequest) -> APIResponse: """ @@ -48,43 +46,9 @@ def get(self, request: SmsMessageGetRequest) -> APIResponse: self.logger.debug("Getting SMS message activity: %s", request.sms_message_id) # Make API request - response = self.client.request( + return self._request( method="GET", path=f"sms-messages/{request.sms_message_id}" ) - return self._create_response(response) - - -class AsyncSmsActivity(AsyncBaseResource): - """Async resource for SMS Activity API endpoints.""" - - async def list(self, request: SmsActivityListRequest) -> APIResponse: - """ - Get a list of SMS activities. - - Args: - request: SMS activity list request - - Returns: - API response with SMS activities - """ - params = request.to_query_params() - response = await self.client.request( - method="GET", path="sms-activity", params=params - ) - return self._create_response(response) - - async def get(self, request: SmsMessageGetRequest) -> APIResponse: - """ - Get activity of a single SMS message. - Args: - request: SMS message get request - - Returns: - API response with SMS message activity - """ - response = await self.client.request( - method="GET", path=f"sms-messages/{request.sms_message_id}" - ) - return self._create_response(response) +AsyncSmsActivity = SmsActivity diff --git a/mailersend/resources/sms_inbounds.py b/mailersend/resources/sms_inbounds.py index 15881d6..b9a3b3a 100644 --- a/mailersend/resources/sms_inbounds.py +++ b/mailersend/resources/sms_inbounds.py @@ -1,6 +1,6 @@ """SMS Inbounds resource.""" -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.sms_inbounds import ( SmsInboundsListRequest, SmsInboundGetRequest, @@ -27,8 +27,7 @@ def list_sms_inbounds(self, request: SmsInboundsListRequest) -> APIResponse: self.logger.debug("Listing SMS inbounds with filters: %s", params) - response = self.client.request(method="GET", path="sms-inbounds", params=params) - return self._create_response(response) + return self._request(method="GET", path="sms-inbounds", params=params) def get_sms_inbound(self, request: SmsInboundGetRequest) -> APIResponse: """Get a single SMS inbound route. @@ -41,12 +40,10 @@ def get_sms_inbound(self, request: SmsInboundGetRequest) -> APIResponse: """ self.logger.debug("Getting SMS inbound: %s", request.sms_inbound_id) - response = self.client.request( + return self._request( method="GET", path=f"sms-inbounds/{request.sms_inbound_id}" ) - return self._create_response(response) - def create_sms_inbound(self, request: SmsInboundCreateRequest) -> APIResponse: """Create a new SMS inbound route. @@ -62,12 +59,10 @@ def create_sms_inbound(self, request: SmsInboundCreateRequest) -> APIResponse: request.sms_number_id, ) - response = self.client.request( + return self._request( method="POST", path="sms-inbounds", body=request.to_request_body() ) - return self._create_response(response) - def update_sms_inbound(self, request: SmsInboundUpdateRequest) -> APIResponse: """Update an existing SMS inbound route. @@ -79,14 +74,12 @@ def update_sms_inbound(self, request: SmsInboundUpdateRequest) -> APIResponse: """ self.logger.debug("Updating SMS inbound: %s", request.sms_inbound_id) - response = self.client.request( + return self._request( method="PUT", path=f"sms-inbounds/{request.sms_inbound_id}", body=request.to_request_body(), ) - return self._create_response(response) - def delete_sms_inbound(self, request: SmsInboundDeleteRequest) -> APIResponse: """Delete an SMS inbound route. @@ -98,85 +91,9 @@ def delete_sms_inbound(self, request: SmsInboundDeleteRequest) -> APIResponse: """ self.logger.debug("Deleting SMS inbound: %s", request.sms_inbound_id) - response = self.client.request( + return self._request( method="DELETE", path=f"sms-inbounds/{request.sms_inbound_id}" ) - return self._create_response(response) - - -class AsyncSmsInbounds(AsyncBaseResource): - """Async SMS Inbounds resource.""" - - async def list_sms_inbounds(self, request: SmsInboundsListRequest) -> APIResponse: - """List SMS inbound routes. - - Args: - request: SmsInboundsListRequest with query parameters - - Returns: - APIResponse: Response containing list of SMS inbound routes - """ - params = request.to_query_params() - response = await self.client.request( - method="GET", path="sms-inbounds", params=params - ) - return self._create_response(response) - async def get_sms_inbound(self, request: SmsInboundGetRequest) -> APIResponse: - """Get a single SMS inbound route. - - Args: - request: SmsInboundGetRequest with inbound ID - - Returns: - APIResponse: Response containing SMS inbound route details - """ - response = await self.client.request( - method="GET", path=f"sms-inbounds/{request.sms_inbound_id}" - ) - return self._create_response(response) - - async def create_sms_inbound(self, request: SmsInboundCreateRequest) -> APIResponse: - """Create a new SMS inbound route. - - Args: - request: SmsInboundCreateRequest with inbound route details - - Returns: - APIResponse: Response containing created SMS inbound route - """ - response = await self.client.request( - method="POST", path="sms-inbounds", body=request.to_request_body() - ) - return self._create_response(response) - - async def update_sms_inbound(self, request: SmsInboundUpdateRequest) -> APIResponse: - """Update an existing SMS inbound route. - - Args: - request: SmsInboundUpdateRequest with inbound ID and updated fields - - Returns: - APIResponse: Response containing updated SMS inbound route - """ - response = await self.client.request( - method="PUT", - path=f"sms-inbounds/{request.sms_inbound_id}", - body=request.to_request_body(), - ) - return self._create_response(response) - - async def delete_sms_inbound(self, request: SmsInboundDeleteRequest) -> APIResponse: - """Delete an SMS inbound route. - - Args: - request: SmsInboundDeleteRequest with inbound ID - - Returns: - APIResponse: Response confirming deletion - """ - response = await self.client.request( - method="DELETE", path=f"sms-inbounds/{request.sms_inbound_id}" - ) - return self._create_response(response) +AsyncSmsInbounds = SmsInbounds diff --git a/mailersend/resources/sms_messages.py b/mailersend/resources/sms_messages.py index 766283a..75ed2e7 100644 --- a/mailersend/resources/sms_messages.py +++ b/mailersend/resources/sms_messages.py @@ -1,6 +1,6 @@ """SMS Messages resource.""" -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.sms_messages import SmsMessagesListRequest, SmsMessageGetRequest from ..models.base import APIResponse @@ -26,9 +26,7 @@ def list_sms_messages(self, request: SmsMessagesListRequest) -> APIResponse: request.query_params.limit, ) - response = self.client.request(method="GET", path="sms-messages", params=params) - - return self._create_response(response) + return self._request(method="GET", path="sms-messages", params=params) def get_sms_message(self, request: SmsMessageGetRequest) -> APIResponse: """ @@ -42,43 +40,9 @@ def get_sms_message(self, request: SmsMessageGetRequest) -> APIResponse: """ self.logger.debug("Getting SMS message: %s", request.sms_message_id) - response = self.client.request( + return self._request( method="GET", path=f"sms-messages/{request.sms_message_id}" ) - return self._create_response(response) - - -class AsyncSmsMessages(AsyncBaseResource): - """Async SMS Messages resource.""" - - async def list_sms_messages(self, request: SmsMessagesListRequest) -> APIResponse: - """ - List SMS messages. - - Args: - request: SmsMessagesListRequest object containing query parameters - - Returns: - APIResponse: Response containing list of SMS messages - """ - params = request.to_query_params() - response = await self.client.request( - method="GET", path="sms-messages", params=params - ) - return self._create_response(response) - - async def get_sms_message(self, request: SmsMessageGetRequest) -> APIResponse: - """ - Get a single SMS message. - Args: - request: SmsMessageGetRequest object containing SMS message ID - - Returns: - APIResponse: Response containing SMS message details - """ - response = await self.client.request( - method="GET", path=f"sms-messages/{request.sms_message_id}" - ) - return self._create_response(response) +AsyncSmsMessages = SmsMessages diff --git a/mailersend/resources/sms_numbers.py b/mailersend/resources/sms_numbers.py index f7c1153..7b24844 100644 --- a/mailersend/resources/sms_numbers.py +++ b/mailersend/resources/sms_numbers.py @@ -1,6 +1,6 @@ """SMS Numbers resource""" -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.sms_numbers import ( SmsNumbersListRequest, SmsNumberGetRequest, @@ -32,9 +32,7 @@ def list(self, request: SmsNumbersListRequest) -> APIResponse: self.logger.debug("Listing SMS phone numbers with params: %s", params) - response = self.client.request(method="GET", path="sms-numbers", params=params) - - return self._create_response(response) + return self._request(method="GET", path="sms-numbers", params=params) def get(self, request: SmsNumberGetRequest) -> APIResponse: """ @@ -48,12 +46,10 @@ def get(self, request: SmsNumberGetRequest) -> APIResponse: """ self.logger.debug("Getting SMS phone number: %s", request.sms_number_id) - response = self.client.request( + return self._request( method="GET", path=f"sms-numbers/{request.sms_number_id}" ) - return self._create_response(response) - def update(self, request: SmsNumberUpdateRequest) -> APIResponse: """ Update a specific SMS phone number. @@ -71,12 +67,10 @@ def update(self, request: SmsNumberUpdateRequest) -> APIResponse: self.logger.debug("Updating SMS phone number: %s", payload) - response = self.client.request( + return self._request( method="PUT", path=f"sms-numbers/{request.sms_number_id}", body=payload ) - return self._create_response(response) - def delete(self, request: SmsNumberDeleteRequest) -> APIResponse: """ Delete a specific SMS phone number. @@ -89,75 +83,9 @@ def delete(self, request: SmsNumberDeleteRequest) -> APIResponse: """ self.logger.debug("Deleting SMS phone number: %s", request.sms_number_id) - response = self.client.request( + return self._request( method="DELETE", path=f"sms-numbers/{request.sms_number_id}" ) - return self._create_response(response) - - -class AsyncSmsNumbers(AsyncBaseResource): - """Async client for the MailerSend SMS Phone Numbers API.""" - - async def list(self, request: SmsNumbersListRequest) -> APIResponse: - """ - Get a list of SMS phone numbers. - - Args: - request: SmsNumbersListRequest with query parameters - - Returns: - APIResponse with SMS phone numbers list and metadata - """ - params = request.to_query_params() - response = await self.client.request( - method="GET", path="sms-numbers", params=params - ) - return self._create_response(response) - - async def get(self, request: SmsNumberGetRequest) -> APIResponse: - """ - Get a specific SMS phone number. - Args: - request: SmsNumberGetRequest with SMS number ID - - Returns: - APIResponse with SMS phone number data and metadata - """ - response = await self.client.request( - method="GET", path=f"sms-numbers/{request.sms_number_id}" - ) - return self._create_response(response) - - async def update(self, request: SmsNumberUpdateRequest) -> APIResponse: - """ - Update a specific SMS phone number. - - Args: - request: SmsNumberUpdateRequest with SMS number ID and update data - - Returns: - APIResponse with updated SMS phone number data and metadata - """ - response = await self.client.request( - method="PUT", - path=f"sms-numbers/{request.sms_number_id}", - body=request.to_json(), - ) - return self._create_response(response) - - async def delete(self, request: SmsNumberDeleteRequest) -> APIResponse: - """ - Delete a specific SMS phone number. - - Args: - request: SmsNumberDeleteRequest with SMS number ID - - Returns: - APIResponse with deletion confirmation and metadata - """ - response = await self.client.request( - method="DELETE", path=f"sms-numbers/{request.sms_number_id}" - ) - return self._create_response(response) +AsyncSmsNumbers = SmsNumbers diff --git a/mailersend/resources/sms_recipients.py b/mailersend/resources/sms_recipients.py index b716679..03ff95a 100644 --- a/mailersend/resources/sms_recipients.py +++ b/mailersend/resources/sms_recipients.py @@ -1,6 +1,6 @@ """SMS Recipients resource.""" -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.sms_recipients import ( SmsRecipientsListRequest, SmsRecipientGetRequest, @@ -29,11 +29,7 @@ def list_sms_recipients(self, request: SmsRecipientsListRequest) -> APIResponse: request.query_params.limit, ) - response = self.client.request( - method="GET", path="sms-recipients", params=params - ) - - return self._create_response(response) + return self._request(method="GET", path="sms-recipients", params=params) def get_sms_recipient(self, request: SmsRecipientGetRequest) -> APIResponse: """ @@ -47,12 +43,10 @@ def get_sms_recipient(self, request: SmsRecipientGetRequest) -> APIResponse: """ self.logger.debug("Getting SMS recipient: %s", request.sms_recipient_id) - response = self.client.request( + return self._request( method="GET", path=f"sms-recipients/{request.sms_recipient_id}" ) - return self._create_response(response) - def update_sms_recipient(self, request: SmsRecipientUpdateRequest) -> APIResponse: """ Update a single SMS recipient. @@ -68,66 +62,11 @@ def update_sms_recipient(self, request: SmsRecipientUpdateRequest) -> APIRespons request.status, ) - response = self.client.request( + return self._request( method="PUT", path=f"sms-recipients/{request.sms_recipient_id}", body=request.to_request_body(), ) - return self._create_response(response) - - -class AsyncSmsRecipients(AsyncBaseResource): - """Async SMS Recipients resource.""" - - async def list_sms_recipients( - self, request: SmsRecipientsListRequest - ) -> APIResponse: - """ - List SMS recipients. - - Args: - request: SmsRecipientsListRequest object containing query parameters - - Returns: - APIResponse: Response containing list of SMS recipients - """ - params = request.to_query_params() - response = await self.client.request( - method="GET", path="sms-recipients", params=params - ) - return self._create_response(response) - - async def get_sms_recipient(self, request: SmsRecipientGetRequest) -> APIResponse: - """ - Get a single SMS recipient. - - Args: - request: SmsRecipientGetRequest object containing SMS recipient ID - - Returns: - APIResponse: Response containing SMS recipient details - """ - response = await self.client.request( - method="GET", path=f"sms-recipients/{request.sms_recipient_id}" - ) - return self._create_response(response) - - async def update_sms_recipient( - self, request: SmsRecipientUpdateRequest - ) -> APIResponse: - """ - Update a single SMS recipient. - Args: - request: SmsRecipientUpdateRequest object containing SMS recipient ID and new status - - Returns: - APIResponse: Response containing updated SMS recipient - """ - response = await self.client.request( - method="PUT", - path=f"sms-recipients/{request.sms_recipient_id}", - body=request.to_request_body(), - ) - return self._create_response(response) +AsyncSmsRecipients = SmsRecipients diff --git a/mailersend/resources/sms_sending.py b/mailersend/resources/sms_sending.py index fc97f63..826403a 100644 --- a/mailersend/resources/sms_sending.py +++ b/mailersend/resources/sms_sending.py @@ -1,6 +1,6 @@ """SMS Sending resource""" -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.sms_sending import SmsSendRequest from ..models.base import APIResponse @@ -27,24 +27,7 @@ def send(self, request: SmsSendRequest) -> APIResponse: self.logger.debug("SMS payload: %s", payload) - response = self.client.request(method="POST", path="sms", body=payload) + return self._request(method="POST", path="sms", body=payload) - return self._create_response(response) - -class AsyncSmsSending(AsyncBaseResource): - """Async client for the MailerSend SMS Sending API.""" - - async def send(self, request: SmsSendRequest) -> APIResponse: - """ - Send an SMS message. - - Args: - request: SmsSendRequest with SMS details - - Returns: - APIResponse with SMS sending response and metadata - """ - payload = request.to_json() - response = await self.client.request(method="POST", path="sms", body=payload) - return self._create_response(response) +AsyncSmsSending = SmsSending diff --git a/mailersend/resources/sms_webhooks.py b/mailersend/resources/sms_webhooks.py index 219f4d2..855ad2a 100644 --- a/mailersend/resources/sms_webhooks.py +++ b/mailersend/resources/sms_webhooks.py @@ -1,6 +1,6 @@ """SMS Webhooks resource.""" -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.sms_webhooks import ( SmsWebhooksListRequest, SmsWebhookGetRequest, @@ -31,9 +31,7 @@ def list_sms_webhooks(self, request: SmsWebhooksListRequest) -> APIResponse: request.query_params.sms_number_id, ) - response = self.client.request(method="GET", path="sms-webhooks", params=params) - - return self._create_response(response) + return self._request(method="GET", path="sms-webhooks", params=params) def get_sms_webhook(self, request: SmsWebhookGetRequest) -> APIResponse: """ @@ -47,12 +45,10 @@ def get_sms_webhook(self, request: SmsWebhookGetRequest) -> APIResponse: """ self.logger.debug("Getting SMS webhook: %s", request.sms_webhook_id) - response = self.client.request( + return self._request( method="GET", path=f"sms-webhooks/{request.sms_webhook_id}" ) - return self._create_response(response) - def create_sms_webhook(self, request: SmsWebhookCreateRequest) -> APIResponse: """ Create an SMS webhook. @@ -68,12 +64,10 @@ def create_sms_webhook(self, request: SmsWebhookCreateRequest) -> APIResponse: request.sms_number_id, ) - response = self.client.request( + return self._request( method="POST", path="sms-webhooks", body=request.to_request_body() ) - return self._create_response(response) - def update_sms_webhook(self, request: SmsWebhookUpdateRequest) -> APIResponse: """ Update an SMS webhook. @@ -86,14 +80,12 @@ def update_sms_webhook(self, request: SmsWebhookUpdateRequest) -> APIResponse: """ self.logger.debug("Updating SMS webhook: %s", request.sms_webhook_id) - response = self.client.request( + return self._request( method="PUT", path=f"sms-webhooks/{request.sms_webhook_id}", body=request.to_request_body(), ) - return self._create_response(response) - def delete_sms_webhook(self, request: SmsWebhookDeleteRequest) -> APIResponse: """ Delete an SMS webhook. @@ -106,88 +98,9 @@ def delete_sms_webhook(self, request: SmsWebhookDeleteRequest) -> APIResponse: """ self.logger.debug("Deleting SMS webhook: %s", request.sms_webhook_id) - response = self.client.request( + return self._request( method="DELETE", path=f"sms-webhooks/{request.sms_webhook_id}" ) - return self._create_response(response) - - -class AsyncSmsWebhooks(AsyncBaseResource): - """Async SMS Webhooks resource.""" - - async def list_sms_webhooks(self, request: SmsWebhooksListRequest) -> APIResponse: - """List SMS webhooks. - - Args: - request: SmsWebhooksListRequest object containing query parameters - - Returns: - APIResponse: Response containing list of SMS webhooks - """ - params = request.to_query_params() - response = await self.client.request( - method="GET", path="sms-webhooks", params=params - ) - return self._create_response(response) - - async def get_sms_webhook(self, request: SmsWebhookGetRequest) -> APIResponse: - """Get a single SMS webhook. - - Args: - request: SmsWebhookGetRequest object containing SMS webhook ID - - Returns: - APIResponse: Response containing SMS webhook details - """ - response = await self.client.request( - method="GET", path=f"sms-webhooks/{request.sms_webhook_id}" - ) - return self._create_response(response) - - async def create_sms_webhook(self, request: SmsWebhookCreateRequest) -> APIResponse: - """ - Create an SMS webhook. - - Args: - request: SmsWebhookCreateRequest object containing webhook data - Returns: - APIResponse: Response containing created SMS webhook - """ - response = await self.client.request( - method="POST", path="sms-webhooks", body=request.to_request_body() - ) - return self._create_response(response) - - async def update_sms_webhook(self, request: SmsWebhookUpdateRequest) -> APIResponse: - """ - Update an SMS webhook. - - Args: - request: SmsWebhookUpdateRequest object containing SMS webhook ID and update data - - Returns: - APIResponse: Response containing updated SMS webhook - """ - response = await self.client.request( - method="PUT", - path=f"sms-webhooks/{request.sms_webhook_id}", - body=request.to_request_body(), - ) - return self._create_response(response) - - async def delete_sms_webhook(self, request: SmsWebhookDeleteRequest) -> APIResponse: - """ - Delete an SMS webhook. - - Args: - request: SmsWebhookDeleteRequest object containing SMS webhook ID - - Returns: - APIResponse: Response confirming deletion - """ - response = await self.client.request( - method="DELETE", path=f"sms-webhooks/{request.sms_webhook_id}" - ) - return self._create_response(response) +AsyncSmsWebhooks = SmsWebhooks diff --git a/mailersend/resources/smtp_users.py b/mailersend/resources/smtp_users.py index a5de7f4..37d0c85 100644 --- a/mailersend/resources/smtp_users.py +++ b/mailersend/resources/smtp_users.py @@ -1,6 +1,6 @@ """SMTP Users API resource.""" -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.base import APIResponse from ..models.smtp_users import ( SmtpUsersListRequest, @@ -33,15 +33,12 @@ def list_smtp_users(self, request: SmtpUsersListRequest) -> APIResponse: params = request.to_query_params() # Make API call - response = self.client.request( + return self._request( method="GET", path=f"domains/{request.domain_id}/smtp-users", params=params, ) - # Create standardized response - return self._create_response(response) - def get_smtp_user(self, request: SmtpUserGetRequest) -> APIResponse: """Get a single SMTP user. @@ -58,14 +55,11 @@ def get_smtp_user(self, request: SmtpUserGetRequest) -> APIResponse: ) # Make API call - response = self.client.request( + return self._request( method="GET", path=f"domains/{request.domain_id}/smtp-users/{request.smtp_user_id}", ) - # Create standardized response - return self._create_response(response) - def create_smtp_user(self, request: SmtpUserCreateRequest) -> APIResponse: """Create an SMTP user. @@ -80,15 +74,12 @@ def create_smtp_user(self, request: SmtpUserCreateRequest) -> APIResponse: ) # Make API call - response = self.client.request( + return self._request( method="POST", path=f"domains/{request.domain_id}/smtp-users", body=request.to_json(), ) - # Create standardized response - return self._create_response(response) - def update_smtp_user(self, request: SmtpUserUpdateRequest) -> APIResponse: """Update an SMTP user. @@ -105,15 +96,12 @@ def update_smtp_user(self, request: SmtpUserUpdateRequest) -> APIResponse: ) # Make API call - response = self.client.request( + return self._request( method="PUT", path=f"domains/{request.domain_id}/smtp-users/{request.smtp_user_id}", body=request.to_json(), ) - # Create standardized response - return self._create_response(response) - def delete_smtp_user(self, request: SmtpUserDeleteRequest) -> APIResponse: """Delete an SMTP user. @@ -130,91 +118,10 @@ def delete_smtp_user(self, request: SmtpUserDeleteRequest) -> APIResponse: ) # Make API call - response = self.client.request( + return self._request( method="DELETE", path=f"domains/{request.domain_id}/smtp-users/{request.smtp_user_id}", ) - # Create standardized response - return self._create_response(response) - - -class AsyncSmtpUsers(AsyncBaseResource): - """Async SMTP Users API resource.""" - - async def list_smtp_users(self, request: SmtpUsersListRequest) -> APIResponse: - """List SMTP users for a domain. - - Args: - request: The list SMTP users request - - Returns: - APIResponse: API response with SMTP users list data - """ - params = request.to_query_params() - response = await self.client.request( - method="GET", path=f"domains/{request.domain_id}/smtp-users", params=params - ) - return self._create_response(response) - - async def get_smtp_user(self, request: SmtpUserGetRequest) -> APIResponse: - """Get a single SMTP user. - - Args: - request: The get SMTP user request - - Returns: - APIResponse: API response with SMTP user data - """ - response = await self.client.request( - method="GET", - path=f"domains/{request.domain_id}/smtp-users/{request.smtp_user_id}", - ) - return self._create_response(response) - - async def create_smtp_user(self, request: SmtpUserCreateRequest) -> APIResponse: - """Create an SMTP user. - - Args: - request: The create SMTP user request - - Returns: - APIResponse: API response with SMTP user creation data - """ - response = await self.client.request( - method="POST", - path=f"domains/{request.domain_id}/smtp-users", - body=request.to_json(), - ) - return self._create_response(response) - - async def update_smtp_user(self, request: SmtpUserUpdateRequest) -> APIResponse: - """Update an SMTP user. - - Args: - request: The update SMTP user request - - Returns: - APIResponse: API response with updated SMTP user data - """ - response = await self.client.request( - method="PUT", - path=f"domains/{request.domain_id}/smtp-users/{request.smtp_user_id}", - body=request.to_json(), - ) - return self._create_response(response) - - async def delete_smtp_user(self, request: SmtpUserDeleteRequest) -> APIResponse: - """Delete an SMTP user. - - Args: - request: The delete SMTP user request - Returns: - APIResponse: API response with delete confirmation - """ - response = await self.client.request( - method="DELETE", - path=f"domains/{request.domain_id}/smtp-users/{request.smtp_user_id}", - ) - return self._create_response(response) +AsyncSmtpUsers = SmtpUsers diff --git a/mailersend/resources/templates.py b/mailersend/resources/templates.py index a3c9f41..e467430 100644 --- a/mailersend/resources/templates.py +++ b/mailersend/resources/templates.py @@ -2,7 +2,7 @@ from typing import Optional -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.base import APIResponse from ..models.templates import ( TemplatesListRequest, @@ -45,10 +45,7 @@ def list_templates( self.logger.debug("Fetching templates with params: %s", params) # Make API call - response = self.client.request(method="GET", path="templates", params=params) - - # Create standardized response - return self._create_response(response) + return self._request(method="GET", path="templates", params=params) def get_template(self, request: TemplateGetRequest) -> APIResponse: """ @@ -63,13 +60,10 @@ def get_template(self, request: TemplateGetRequest) -> APIResponse: self.logger.debug("Template get request: %s", request) # Make API call - response = self.client.request( + return self._request( method="GET", path=f"templates/{request.template_id}" ) - # Create standardized response - return self._create_response(response) - def delete_template(self, request: TemplateDeleteRequest) -> APIResponse: """ Delete a template. @@ -84,63 +78,9 @@ def delete_template(self, request: TemplateDeleteRequest) -> APIResponse: self.logger.debug("Deleting template: %s", request.template_id) # Make API call - response = self.client.request( + return self._request( method="DELETE", path=f"templates/{request.template_id}" ) - # Create standardized response - return self._create_response(response) - - -class AsyncTemplates(AsyncBaseResource): - """Async client for interacting with the MailerSend Templates API.""" - - async def list_templates( - self, request: Optional[TemplatesListRequest] = None - ) -> APIResponse: - """ - Retrieve a list of templates. - - Args: - request: Optional TemplatesListRequest with filtering and pagination options - - Returns: - APIResponse with TemplatesListResponse data - """ - if request is None: - request = TemplatesListRequest() - params = request.to_query_params() - response = await self.client.request( - method="GET", path="templates", params=params - ) - return self._create_response(response) - async def get_template(self, request: TemplateGetRequest) -> APIResponse: - """ - Retrieve information about a single template. - - Args: - request: TemplateGetRequest with template ID - - Returns: - APIResponse with TemplateResponse data - """ - response = await self.client.request( - method="GET", path=f"templates/{request.template_id}" - ) - return self._create_response(response) - - async def delete_template(self, request: TemplateDeleteRequest) -> APIResponse: - """ - Delete a template. - - Args: - request: TemplateDeleteRequest with template ID to delete - - Returns: - APIResponse with empty data - """ - response = await self.client.request( - method="DELETE", path=f"templates/{request.template_id}" - ) - return self._create_response(response) +AsyncTemplates = Templates diff --git a/mailersend/resources/tokens.py b/mailersend/resources/tokens.py index b2bf362..456e396 100644 --- a/mailersend/resources/tokens.py +++ b/mailersend/resources/tokens.py @@ -1,6 +1,6 @@ """Tokens API resource.""" -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.base import APIResponse from ..models.tokens import ( TokensListRequest, @@ -32,10 +32,7 @@ def list_tokens(self, request: TokensListRequest) -> APIResponse: params = request.to_query_params() # Make API call - response = self.client.request(method="GET", path="token", params=params) - - # Create standardized response - return self._create_response(response) + return self._request(method="GET", path="token", params=params) def get_token(self, request: TokenGetRequest) -> APIResponse: """Get a single API token. @@ -49,10 +46,7 @@ def get_token(self, request: TokenGetRequest) -> APIResponse: self.logger.info("Getting token: %s", request.token_id) # Make API call - response = self.client.request(method="GET", path=f"token/{request.token_id}") - - # Create standardized response - return self._create_response(response) + return self._request(method="GET", path=f"token/{request.token_id}") def create_token(self, request: TokenCreateRequest) -> APIResponse: """Create an API token. @@ -68,12 +62,7 @@ def create_token(self, request: TokenCreateRequest) -> APIResponse: ) # Make API call - response = self.client.request( - method="POST", path="token", body=request.to_json() - ) - - # Create standardized response - return self._create_response(response) + return self._request(method="POST", path="token", body=request.to_json()) def update_token(self, request: TokenUpdateRequest) -> APIResponse: """Update an API token status. @@ -89,15 +78,12 @@ def update_token(self, request: TokenUpdateRequest) -> APIResponse: ) # Make API call - response = self.client.request( + return self._request( method="PUT", path=f"token/{request.token_id}/settings", body=request.to_json(), ) - # Create standardized response - return self._create_response(response) - def update_token_name(self, request: TokenUpdateNameRequest) -> APIResponse: """Update an API token name. @@ -110,13 +96,10 @@ def update_token_name(self, request: TokenUpdateNameRequest) -> APIResponse: self.logger.info("Updating token name: {request.token_id} to: %s", request.name) # Make API call - response = self.client.request( + return self._request( method="PUT", path=f"token/{request.token_id}", body=request.to_json() ) - # Create standardized response - return self._create_response(response) - def delete_token(self, request: TokenDeleteRequest) -> APIResponse: """Delete an API token. @@ -129,98 +112,7 @@ def delete_token(self, request: TokenDeleteRequest) -> APIResponse: self.logger.info("Deleting token: %s", request.token_id) # Make API call - response = self.client.request( - method="DELETE", path=f"token/{request.token_id}" - ) - - # Create standardized response - return self._create_response(response) - - -class AsyncTokens(AsyncBaseResource): - """Async Tokens API resource.""" - - async def list_tokens(self, request: TokensListRequest) -> APIResponse: - """List API tokens. - - Args: - request: The list tokens request - - Returns: - APIResponse: API response with tokens list data - """ - params = request.to_query_params() - response = await self.client.request(method="GET", path="token", params=params) - return self._create_response(response) - - async def get_token(self, request: TokenGetRequest) -> APIResponse: - """Get a single API token. - - Args: - request: The get token request - - Returns: - APIResponse: API response with token data - """ - response = await self.client.request( - method="GET", path=f"token/{request.token_id}" - ) - return self._create_response(response) - - async def create_token(self, request: TokenCreateRequest) -> APIResponse: - """Create an API token. - - Args: - request: The create token request - - Returns: - APIResponse: API response with token creation data - """ - response = await self.client.request( - method="POST", path="token", body=request.to_json() - ) - return self._create_response(response) - - async def update_token(self, request: TokenUpdateRequest) -> APIResponse: - """Update an API token status. - - Args: - request: The update token request - - Returns: - APIResponse: API response with update confirmation - """ - response = await self.client.request( - method="PUT", - path=f"token/{request.token_id}/settings", - body=request.to_json(), - ) - return self._create_response(response) - - async def update_token_name(self, request: TokenUpdateNameRequest) -> APIResponse: - """Update an API token name. - - Args: - request: The update token name request - - Returns: - APIResponse: API response with update confirmation - """ - response = await self.client.request( - method="PUT", path=f"token/{request.token_id}", body=request.to_json() - ) - return self._create_response(response) - - async def delete_token(self, request: TokenDeleteRequest) -> APIResponse: - """Delete an API token. + return self._request(method="DELETE", path=f"token/{request.token_id}") - Args: - request: The delete token request - Returns: - APIResponse: API response with delete confirmation - """ - response = await self.client.request( - method="DELETE", path=f"token/{request.token_id}" - ) - return self._create_response(response) +AsyncTokens = Tokens diff --git a/mailersend/resources/users.py b/mailersend/resources/users.py index 4d34c3c..18a99c6 100644 --- a/mailersend/resources/users.py +++ b/mailersend/resources/users.py @@ -1,6 +1,6 @@ """Users API resource.""" -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.base import APIResponse from ..models.users import ( UsersListRequest, @@ -37,10 +37,7 @@ def list_users(self, request: UsersListRequest) -> APIResponse: params = request.to_query_params() # Make API call - response = self.client.request(method="GET", path="users", params=params) - - # Create standardized response - return self._create_response(response) + return self._request(method="GET", path="users", params=params) def get_user(self, request: UserGetRequest) -> APIResponse: """Get a single account user. @@ -54,10 +51,7 @@ def get_user(self, request: UserGetRequest) -> APIResponse: self.logger.debug("Getting user: %s", request.user_id) # Make API call - response = self.client.request(method="GET", path=f"users/{request.user_id}") - - # Create standardized response - return self._create_response(response) + return self._request(method="GET", path=f"users/{request.user_id}") def invite_user(self, request: UserInviteRequest) -> APIResponse: """Invite a user to account. @@ -73,12 +67,7 @@ def invite_user(self, request: UserInviteRequest) -> APIResponse: ) # Make API call - response = self.client.request( - method="POST", path="users", body=request.to_json() - ) - - # Create standardized response - return self._create_response(response) + return self._request(method="POST", path="users", body=request.to_json()) def update_user(self, request: UserUpdateRequest) -> APIResponse: """Update account user. @@ -94,13 +83,10 @@ def update_user(self, request: UserUpdateRequest) -> APIResponse: ) # Make API call - response = self.client.request( + return self._request( method="PUT", path=f"users/{request.user_id}", body=request.to_json() ) - # Create standardized response - return self._create_response(response) - def delete_user(self, request: UserDeleteRequest) -> APIResponse: """Delete account user. @@ -113,10 +99,7 @@ def delete_user(self, request: UserDeleteRequest) -> APIResponse: self.logger.debug("Deleting user: %s", request.user_id) # Make API call - response = self.client.request(method="DELETE", path=f"users/{request.user_id}") - - # Create standardized response - return self._create_response(response, None) + return self._request(method="DELETE", path=f"users/{request.user_id}", data=lambda r: None) def list_invites(self, request: InvitesListRequest) -> APIResponse: """Get a list of invites. @@ -137,10 +120,7 @@ def list_invites(self, request: InvitesListRequest) -> APIResponse: params = request.to_query_params() # Make API call - response = self.client.request(method="GET", path="invites", params=params) - - # Create standardized response - return self._create_response(response) + return self._request(method="GET", path="invites", params=params) def get_invite(self, request: InviteGetRequest) -> APIResponse: """Get a single invite. @@ -154,12 +134,7 @@ def get_invite(self, request: InviteGetRequest) -> APIResponse: self.logger.debug("Getting invite: %s", request.invite_id) # Make API call - response = self.client.request( - method="GET", path=f"invites/{request.invite_id}" - ) - - # Create standardized response - return self._create_response(response) + return self._request(method="GET", path=f"invites/{request.invite_id}") def resend_invite(self, request: InviteResendRequest) -> APIResponse: """Resend an invite. @@ -173,12 +148,7 @@ def resend_invite(self, request: InviteResendRequest) -> APIResponse: self.logger.debug("Resending invite: %s", request.invite_id) # Make API call - response = self.client.request( - method="POST", path=f"invites/{request.invite_id}/resend" - ) - - # Create standardized response - return self._create_response(response) + return self._request(method="POST", path=f"invites/{request.invite_id}/resend") def cancel_invite(self, request: InviteCancelRequest) -> APIResponse: """Cancel an invite. @@ -192,139 +162,7 @@ def cancel_invite(self, request: InviteCancelRequest) -> APIResponse: self.logger.debug("Canceling invite: %s", request.invite_id) # Make API call - response = self.client.request( - method="DELETE", path=f"invites/{request.invite_id}" - ) - - # Create standardized response - return self._create_response(response, None) - + return self._request(method="DELETE", path=f"invites/{request.invite_id}", data=lambda r: None) -class AsyncUsers(AsyncBaseResource): - """Async Users API resource.""" - async def list_users(self, request: UsersListRequest) -> APIResponse: - """Get a list of account users. - - Args: - request: The list users request - - Returns: - APIResponse: API response with users list data - """ - params = request.to_query_params() - response = await self.client.request(method="GET", path="users", params=params) - return self._create_response(response) - - async def get_user(self, request: UserGetRequest) -> APIResponse: - """Get a single account user. - - Args: - request: The get user request - - Returns: - APIResponse: API response with user data - """ - response = await self.client.request( - method="GET", path=f"users/{request.user_id}" - ) - return self._create_response(response) - - async def invite_user(self, request: UserInviteRequest) -> APIResponse: - """Invite a user to account. - - Args: - request: The user invite request - - Returns: - APIResponse: API response with invite data - """ - response = await self.client.request( - method="POST", path="users", body=request.to_json() - ) - return self._create_response(response) - - async def update_user(self, request: UserUpdateRequest) -> APIResponse: - """Update account user. - - Args: - request: The user update request - - Returns: - APIResponse: API response with updated user data - """ - response = await self.client.request( - method="PUT", path=f"users/{request.user_id}", body=request.to_json() - ) - return self._create_response(response) - - async def delete_user(self, request: UserDeleteRequest) -> APIResponse: - """Delete account user. - - Args: - request: The user delete request - - Returns: - APIResponse: API response with delete confirmation - """ - response = await self.client.request( - method="DELETE", path=f"users/{request.user_id}" - ) - return self._create_response(response, None) - - async def list_invites(self, request: InvitesListRequest) -> APIResponse: - """Get a list of invites. - - Args: - request: The list invites request - - Returns: - APIResponse: API response with invites list data - """ - params = request.to_query_params() - response = await self.client.request( - method="GET", path="invites", params=params - ) - return self._create_response(response) - - async def get_invite(self, request: InviteGetRequest) -> APIResponse: - """Get a single invite. - - Args: - request: The get invite request - - Returns: - APIResponse: API response with invite data - """ - response = await self.client.request( - method="GET", path=f"invites/{request.invite_id}" - ) - return self._create_response(response) - - async def resend_invite(self, request: InviteResendRequest) -> APIResponse: - """Resend an invite. - - Args: - request: The invite resend request - - Returns: - APIResponse: API response with resent invite data - """ - response = await self.client.request( - method="POST", path=f"invites/{request.invite_id}/resend" - ) - return self._create_response(response) - - async def cancel_invite(self, request: InviteCancelRequest) -> APIResponse: - """Cancel an invite. - - Args: - request: The invite cancel request - - Returns: - APIResponse: API response with cancel confirmation - """ - response = await self.client.request( - method="DELETE", path=f"invites/{request.invite_id}" - ) - return self._create_response(response, None) +AsyncUsers = Users diff --git a/mailersend/resources/webhooks.py b/mailersend/resources/webhooks.py index 418c2ea..1e874af 100644 --- a/mailersend/resources/webhooks.py +++ b/mailersend/resources/webhooks.py @@ -1,6 +1,6 @@ """Webhooks resource for MailerSend SDK.""" -from .base import AsyncBaseResource, BaseResource +from .base import BaseResource from ..models.base import APIResponse from ..models.webhooks import ( WebhooksListRequest, @@ -32,10 +32,7 @@ def list_webhooks(self, request: WebhooksListRequest) -> APIResponse: self.logger.debug("Listing webhooks with params: %s", params) # Make API call - response = self.client.request(method="GET", path="webhooks", params=params) - - # Create standardized response - return self._create_response(response) + return self._request(method="GET", path="webhooks", params=params) def get_webhook(self, request: WebhookGetRequest) -> APIResponse: """Get a single webhook by ID. @@ -50,13 +47,10 @@ def get_webhook(self, request: WebhookGetRequest) -> APIResponse: self.logger.debug("Webhook get request: %s", request) # Make API call - response = self.client.request( + return self._request( method="GET", path=f"webhooks/{request.webhook_id}" ) - # Create standardized response - return self._create_response(response) - def create_webhook(self, request: WebhookCreateRequest) -> APIResponse: """Create a new webhook. @@ -74,10 +68,7 @@ def create_webhook(self, request: WebhookCreateRequest) -> APIResponse: self.logger.debug("Creating webhook: %s", request.name) # Make API call - response = self.client.request(method="POST", path="webhooks", body=data) - - # Create standardized response - return self._create_response(response) + return self._request(method="POST", path="webhooks", body=data) def update_webhook(self, request: WebhookUpdateRequest) -> APIResponse: """Update an existing webhook. @@ -97,13 +88,10 @@ def update_webhook(self, request: WebhookUpdateRequest) -> APIResponse: self.logger.debug("Updating webhook: %s", data) # Make API call - response = self.client.request( + return self._request( method="PUT", path=f"webhooks/{request.webhook_id}", body=data ) - # Create standardized response - return self._create_response(response) - def delete_webhook(self, request: WebhookDeleteRequest) -> APIResponse: """Delete a webhook. @@ -117,84 +105,9 @@ def delete_webhook(self, request: WebhookDeleteRequest) -> APIResponse: self.logger.debug("Webhook delete request: %s", request) # Make API call - response = self.client.request( + return self._request( method="DELETE", path=f"webhooks/{request.webhook_id}" ) - # Create standardized response - return self._create_response(response) - - -class AsyncWebhooks(AsyncBaseResource): - """Async Webhooks API resource.""" - - async def list_webhooks(self, request: WebhooksListRequest) -> APIResponse: - """List webhooks for a domain. - - Args: - request: The webhooks list request - - Returns: - APIResponse with WebhooksListResponse data - """ - params = request.to_query_params() - response = await self.client.request( - method="GET", path="webhooks", params=params - ) - return self._create_response(response) - - async def get_webhook(self, request: WebhookGetRequest) -> APIResponse: - """Get a single webhook by ID. - - Args: - request: The webhook get request - - Returns: - APIResponse with WebhookResponse data - """ - response = await self.client.request( - method="GET", path=f"webhooks/{request.webhook_id}" - ) - return self._create_response(response) - - async def create_webhook(self, request: WebhookCreateRequest) -> APIResponse: - """Create a new webhook. - - Args: - request: The webhook create request - - Returns: - APIResponse with WebhookResponse data - """ - data = request.model_dump(exclude_none=True) - response = await self.client.request(method="POST", path="webhooks", body=data) - return self._create_response(response) - - async def update_webhook(self, request: WebhookUpdateRequest) -> APIResponse: - """Update an existing webhook. - - Args: - request: The webhook update request - - Returns: - APIResponse with WebhookResponse data - """ - data = request.model_dump(exclude_none=True, exclude={"webhook_id"}) - response = await self.client.request( - method="PUT", path=f"webhooks/{request.webhook_id}", body=data - ) - return self._create_response(response) - - async def delete_webhook(self, request: WebhookDeleteRequest) -> APIResponse: - """Delete a webhook. - Args: - request: The webhook delete request - - Returns: - APIResponse with empty data - """ - response = await self.client.request( - method="DELETE", path=f"webhooks/{request.webhook_id}" - ) - return self._create_response(response) +AsyncWebhooks = Webhooks diff --git a/tests/unit/test_analytics_resource.py b/tests/unit/test_analytics_resource.py index e7aa540..8406ba3 100644 --- a/tests/unit/test_analytics_resource.py +++ b/tests/unit/test_analytics_resource.py @@ -56,11 +56,12 @@ def test_get_activity_by_date_success(self): assert isinstance(result, APIResponse) self.mock_client.request.assert_called_once() call_args = self.mock_client.request.call_args - assert call_args[0] == ("GET", "analytics/date") - assert "params" in call_args[1] + assert call_args.kwargs["method"] == "GET" + assert call_args.kwargs["path"] == "analytics/date" + assert "params" in call_args.kwargs # Check that the right parameters were passed - params = call_args[1]["params"] + params = call_args.kwargs["params"] assert "date_from" in params assert "date_to" in params assert "tags[]" in params @@ -122,10 +123,11 @@ def test_get_opens_by_country_success(self): assert isinstance(result, APIResponse) self.mock_client.request.assert_called_once() call_args = self.mock_client.request.call_args - assert call_args[0] == ("GET", "analytics/country") + assert call_args.kwargs["method"] == "GET" + assert call_args.kwargs["path"] == "analytics/country" # Check that excluded fields are not present - params = call_args[1]["params"] + params = call_args.kwargs["params"] assert "event" not in params assert "group_by" not in params assert "date_from" in params @@ -162,7 +164,8 @@ def test_get_opens_by_user_agent_success(self): assert isinstance(result, APIResponse) self.mock_client.request.assert_called_once() call_args = self.mock_client.request.call_args - assert call_args[0] == ("GET", "analytics/ua-name") + assert call_args.kwargs["method"] == "GET" + assert call_args.kwargs["path"] == "analytics/ua-name" def test_get_opens_by_reading_environment_success(self): """Test successful opens by reading environment request""" @@ -196,7 +199,8 @@ def test_get_opens_by_reading_environment_success(self): assert isinstance(result, APIResponse) self.mock_client.request.assert_called_once() call_args = self.mock_client.request.call_args - assert call_args[0] == ("GET", "analytics/ua-type") + assert call_args.kwargs["method"] == "GET" + assert call_args.kwargs["path"] == "analytics/ua-type" def test_build_query_params_basic(self): """Test basic query parameter building""" diff --git a/tests/unit/test_async_analytics_resource.py b/tests/unit/test_async_analytics_resource.py index cff82ab..c9ae5c1 100644 --- a/tests/unit/test_async_analytics_resource.py +++ b/tests/unit/test_async_analytics_resource.py @@ -38,7 +38,8 @@ async def test_get_activity_by_date_returns_api_response(self): async def test_get_activity_by_date_calls_correct_endpoint(self): await self.resource.get_activity_by_date(_make_request()) call = self.mock_client.request.call_args - assert call.args == ("GET", "analytics/date") + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "analytics/date" async def test_get_activity_by_date_passes_params(self): await self.resource.get_activity_by_date(_make_request()) @@ -54,7 +55,8 @@ async def test_get_opens_by_country_returns_api_response(self): async def test_get_opens_by_country_calls_correct_endpoint(self): await self.resource.get_opens_by_country(_make_request()) call = self.mock_client.request.call_args - assert call.args == ("GET", "analytics/country") + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "analytics/country" async def test_get_opens_by_country_excludes_event_and_group_by(self): await self.resource.get_opens_by_country(_make_request()) @@ -71,7 +73,8 @@ async def test_get_opens_by_user_agent_returns_api_response(self): async def test_get_opens_by_user_agent_calls_correct_endpoint(self): await self.resource.get_opens_by_user_agent(_make_request()) call = self.mock_client.request.call_args - assert call.args == ("GET", "analytics/ua-name") + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "analytics/ua-name" async def test_get_opens_by_reading_environment_returns_api_response(self): result = await self.resource.get_opens_by_reading_environment(_make_request()) @@ -80,4 +83,5 @@ async def test_get_opens_by_reading_environment_returns_api_response(self): async def test_get_opens_by_reading_environment_calls_correct_endpoint(self): await self.resource.get_opens_by_reading_environment(_make_request()) call = self.mock_client.request.call_args - assert call.args == ("GET", "analytics/ua-type") + assert call.kwargs["method"] == "GET" + assert call.kwargs["path"] == "analytics/ua-type" From c3d7fbe8f190338678ce576d0e036d51511c3cb5 Mon Sep 17 00:00:00 2001 From: rocribera Date: Tue, 26 May 2026 15:43:27 +0200 Subject: [PATCH 3/5] feat: remove aliasing --- mailersend/async_client.py | 96 +++++++++---------- mailersend/resources/__init__.py | 77 +++++---------- mailersend/resources/activity.py | 1 - mailersend/resources/analytics.py | 1 - mailersend/resources/base.py | 3 - mailersend/resources/dmarc_monitoring.py | 1 - mailersend/resources/domains.py | 1 - mailersend/resources/email.py | 1 - mailersend/resources/email_verification.py | 1 - mailersend/resources/identities.py | 1 - mailersend/resources/inbound.py | 1 - mailersend/resources/messages.py | 1 - mailersend/resources/other.py | 1 - mailersend/resources/recipients.py | 1 - mailersend/resources/schedules.py | 1 - mailersend/resources/sms_activity.py | 1 - mailersend/resources/sms_inbounds.py | 1 - mailersend/resources/sms_messages.py | 1 - mailersend/resources/sms_numbers.py | 1 - mailersend/resources/sms_recipients.py | 1 - mailersend/resources/sms_sending.py | 1 - mailersend/resources/sms_webhooks.py | 1 - mailersend/resources/smtp_users.py | 1 - mailersend/resources/templates.py | 1 - mailersend/resources/tokens.py | 1 - mailersend/resources/users.py | 1 - mailersend/resources/webhooks.py | 1 - tests/unit/test_async_activity_resource.py | 8 +- tests/unit/test_async_analytics_resource.py | 8 +- .../test_async_dmarc_monitoring_resource.py | 8 +- tests/unit/test_async_domains_resource.py | 8 +- tests/unit/test_async_email_resource.py | 8 +- .../test_async_email_verification_resource.py | 8 +- tests/unit/test_async_identities_resource.py | 8 +- tests/unit/test_async_inbound_resource.py | 8 +- tests/unit/test_async_messages_resource.py | 8 +- tests/unit/test_async_other_resource.py | 8 +- tests/unit/test_async_recipients_resource.py | 8 +- tests/unit/test_async_schedules_resource.py | 8 +- .../unit/test_async_sms_activity_resource.py | 8 +- .../unit/test_async_sms_inbounds_resource.py | 8 +- .../unit/test_async_sms_messages_resource.py | 8 +- tests/unit/test_async_sms_numbers_resource.py | 8 +- .../test_async_sms_recipients_resource.py | 8 +- tests/unit/test_async_sms_sending_resource.py | 8 +- .../unit/test_async_sms_webhooks_resource.py | 8 +- tests/unit/test_async_smtp_users_resource.py | 8 +- tests/unit/test_async_templates_resource.py | 8 +- tests/unit/test_async_tokens_resource.py | 8 +- tests/unit/test_async_users_resource.py | 8 +- tests/unit/test_async_webhooks_resource.py | 8 +- 51 files changed, 169 insertions(+), 223 deletions(-) diff --git a/mailersend/async_client.py b/mailersend/async_client.py index 4a53005..e806f64 100644 --- a/mailersend/async_client.py +++ b/mailersend/async_client.py @@ -15,30 +15,30 @@ ResourceNotFoundError, ServerError, ) -from .resources.activity import AsyncActivity -from .resources.analytics import AsyncAnalytics -from .resources.dmarc_monitoring import AsyncDmarcMonitoring -from .resources.domains import AsyncDomains -from .resources.email import AsyncEmail -from .resources.email_verification import AsyncEmailVerification -from .resources.identities import AsyncIdentitiesResource -from .resources.inbound import AsyncInboundResource -from .resources.messages import AsyncMessages -from .resources.other import AsyncOther -from .resources.recipients import AsyncRecipients -from .resources.schedules import AsyncSchedules -from .resources.sms_activity import AsyncSmsActivity -from .resources.sms_inbounds import AsyncSmsInbounds -from .resources.sms_messages import AsyncSmsMessages -from .resources.sms_numbers import AsyncSmsNumbers -from .resources.sms_recipients import AsyncSmsRecipients -from .resources.sms_sending import AsyncSmsSending -from .resources.sms_webhooks import AsyncSmsWebhooks -from .resources.smtp_users import AsyncSmtpUsers -from .resources.templates import AsyncTemplates -from .resources.tokens import AsyncTokens -from .resources.users import AsyncUsers -from .resources.webhooks import AsyncWebhooks +from .resources.activity import Activity +from .resources.analytics import Analytics +from .resources.dmarc_monitoring import DmarcMonitoring +from .resources.domains import Domains +from .resources.email import Email +from .resources.email_verification import EmailVerification +from .resources.identities import IdentitiesResource +from .resources.inbound import InboundResource +from .resources.messages import Messages +from .resources.other import Other +from .resources.recipients import Recipients +from .resources.schedules import Schedules +from .resources.sms_activity import SmsActivity +from .resources.sms_inbounds import SmsInbounds +from .resources.sms_messages import SmsMessages +from .resources.sms_numbers import SmsNumbers +from .resources.sms_recipients import SmsRecipients +from .resources.sms_sending import SmsSending +from .resources.sms_webhooks import SmsWebhooks +from .resources.smtp_users import SmtpUsers +from .resources.templates import Templates +from .resources.tokens import Tokens +from .resources.users import Users +from .resources.webhooks import Webhooks from .logging import get_logger, RequestLogger _RETRY_STATUSES = frozenset([429, 500, 502, 503, 504]) @@ -114,30 +114,30 @@ def __init__( timeout=self.timeout, ) - self.emails = AsyncEmail(self) - self.activities = AsyncActivity(self) - self.analytics = AsyncAnalytics(self) - self.domains = AsyncDomains(self) - self.identities = AsyncIdentitiesResource(self) - self.inbound = AsyncInboundResource(self) - self.templates = AsyncTemplates(self) - self.tokens = AsyncTokens(self) - self.webhooks = AsyncWebhooks(self) - self.email_verification = AsyncEmailVerification(self) - self.users = AsyncUsers(self) - self.messages = AsyncMessages(self) - self.recipients = AsyncRecipients(self) - self.schedules = AsyncSchedules(self) - self.sms_messages = AsyncSmsMessages(self) - self.smtp_users = AsyncSmtpUsers(self) - self.sms_sending = AsyncSmsSending(self) - self.sms_numbers = AsyncSmsNumbers(self) - self.sms_activity = AsyncSmsActivity(self) - self.sms_inbounds = AsyncSmsInbounds(self) - self.sms_recipients = AsyncSmsRecipients(self) - self.sms_webhooks = AsyncSmsWebhooks(self) - self.api_quota = AsyncOther(self) - self.dmarc_monitoring = AsyncDmarcMonitoring(self) + self.emails = Email(self) + self.activities = Activity(self) + self.analytics = Analytics(self) + self.domains = Domains(self) + self.identities = IdentitiesResource(self) + self.inbound = InboundResource(self) + self.templates = Templates(self) + self.tokens = Tokens(self) + self.webhooks = Webhooks(self) + self.email_verification = EmailVerification(self) + self.users = Users(self) + self.messages = Messages(self) + self.recipients = Recipients(self) + self.schedules = Schedules(self) + self.sms_messages = SmsMessages(self) + self.smtp_users = SmtpUsers(self) + self.sms_sending = SmsSending(self) + self.sms_numbers = SmsNumbers(self) + self.sms_activity = SmsActivity(self) + self.sms_inbounds = SmsInbounds(self) + self.sms_recipients = SmsRecipients(self) + self.sms_webhooks = SmsWebhooks(self) + self.api_quota = Other(self) + self.dmarc_monitoring = DmarcMonitoring(self) self.logger.info("AsyncMailerSendClient initialized successfully") diff --git a/mailersend/resources/__init__.py b/mailersend/resources/__init__.py index d51d779..594aa84 100644 --- a/mailersend/resources/__init__.py +++ b/mailersend/resources/__init__.py @@ -2,34 +2,33 @@ API resource classes for interacting with specific MailerSend API endpoints. """ -from .base import AsyncBaseResource, BaseResource -from .email import AsyncEmail, Email -from .activity import AsyncActivity, Activity -from .analytics import AsyncAnalytics, Analytics -from .domains import AsyncDomains, Domains -from .identities import AsyncIdentitiesResource, IdentitiesResource -from .inbound import AsyncInboundResource, InboundResource -from .messages import AsyncMessages, Messages -from .schedules import AsyncSchedules, Schedules -from .recipients import AsyncRecipients, Recipients -from .templates import AsyncTemplates, Templates -from .tokens import AsyncTokens, Tokens -from .webhooks import AsyncWebhooks, Webhooks -from .email_verification import AsyncEmailVerification, EmailVerification -from .users import AsyncUsers, Users -from .sms_messages import AsyncSmsMessages, SmsMessages -from .sms_numbers import AsyncSmsNumbers, SmsNumbers -from .sms_activity import AsyncSmsActivity, SmsActivity -from .sms_sending import AsyncSmsSending, SmsSending -from .sms_recipients import AsyncSmsRecipients, SmsRecipients -from .sms_webhooks import AsyncSmsWebhooks, SmsWebhooks -from .sms_inbounds import AsyncSmsInbounds, SmsInbounds -from .other import AsyncOther, Other -from .dmarc_monitoring import AsyncDmarcMonitoring, DmarcMonitoring -from .smtp_users import AsyncSmtpUsers, SmtpUsers +from .base import BaseResource +from .email import Email +from .activity import Activity +from .analytics import Analytics +from .domains import Domains +from .identities import IdentitiesResource +from .inbound import InboundResource +from .messages import Messages +from .schedules import Schedules +from .recipients import Recipients +from .templates import Templates +from .tokens import Tokens +from .webhooks import Webhooks +from .email_verification import EmailVerification +from .users import Users +from .sms_messages import SmsMessages +from .sms_numbers import SmsNumbers +from .sms_activity import SmsActivity +from .sms_sending import SmsSending +from .sms_recipients import SmsRecipients +from .sms_webhooks import SmsWebhooks +from .sms_inbounds import SmsInbounds +from .other import Other +from .dmarc_monitoring import DmarcMonitoring +from .smtp_users import SmtpUsers __all__ = [ - # Sync resources "BaseResource", "Email", "Activity", @@ -55,30 +54,4 @@ "SmtpUsers", "Other", "DmarcMonitoring", - # Async resources - "AsyncBaseResource", - "AsyncEmail", - "AsyncActivity", - "AsyncAnalytics", - "AsyncDomains", - "AsyncIdentitiesResource", - "AsyncInboundResource", - "AsyncMessages", - "AsyncSchedules", - "AsyncRecipients", - "AsyncTemplates", - "AsyncTokens", - "AsyncWebhooks", - "AsyncEmailVerification", - "AsyncUsers", - "AsyncSmsMessages", - "AsyncSmsNumbers", - "AsyncSmsActivity", - "AsyncSmsSending", - "AsyncSmsRecipients", - "AsyncSmsWebhooks", - "AsyncSmsInbounds", - "AsyncSmtpUsers", - "AsyncOther", - "AsyncDmarcMonitoring", ] diff --git a/mailersend/resources/activity.py b/mailersend/resources/activity.py index 1a5ce63..41e7475 100644 --- a/mailersend/resources/activity.py +++ b/mailersend/resources/activity.py @@ -48,4 +48,3 @@ def get_single(self, request: SingleActivityRequest) -> APIResponse: return self._request(method="GET", path=f"activities/{request.activity_id}") -AsyncActivity = Activity diff --git a/mailersend/resources/analytics.py b/mailersend/resources/analytics.py index ed93dde..5c94687 100644 --- a/mailersend/resources/analytics.py +++ b/mailersend/resources/analytics.py @@ -123,4 +123,3 @@ def _build_query_params( return params -AsyncAnalytics = Analytics diff --git a/mailersend/resources/base.py b/mailersend/resources/base.py index 6d6d871..7977955 100644 --- a/mailersend/resources/base.py +++ b/mailersend/resources/base.py @@ -128,7 +128,4 @@ def _process_response( return response_data -class AsyncBaseResource(BaseResource): - """Base class for all async API resources.""" - pass diff --git a/mailersend/resources/dmarc_monitoring.py b/mailersend/resources/dmarc_monitoring.py index 0333d28..9382cf4 100644 --- a/mailersend/resources/dmarc_monitoring.py +++ b/mailersend/resources/dmarc_monitoring.py @@ -205,4 +205,3 @@ def remove_ip_favorite( ) -AsyncDmarcMonitoring = DmarcMonitoring diff --git a/mailersend/resources/domains.py b/mailersend/resources/domains.py index 23a4ee8..abeed74 100644 --- a/mailersend/resources/domains.py +++ b/mailersend/resources/domains.py @@ -186,4 +186,3 @@ def get_domain_verification_status( ) -AsyncDomains = Domains diff --git a/mailersend/resources/email.py b/mailersend/resources/email.py index 949c614..4b641e2 100644 --- a/mailersend/resources/email.py +++ b/mailersend/resources/email.py @@ -75,4 +75,3 @@ def get_bulk_status(self, bulk_email_id: str) -> APIResponse: return self._request(method="GET", path=f"bulk-email/{bulk_email_id}") -AsyncEmail = Email diff --git a/mailersend/resources/email_verification.py b/mailersend/resources/email_verification.py index 28f9585..1c8e629 100644 --- a/mailersend/resources/email_verification.py +++ b/mailersend/resources/email_verification.py @@ -202,4 +202,3 @@ def get_results(self, request: EmailVerificationResultsRequest) -> APIResponse: ) -AsyncEmailVerification = EmailVerification diff --git a/mailersend/resources/identities.py b/mailersend/resources/identities.py index cc7c2eb..8598df0 100644 --- a/mailersend/resources/identities.py +++ b/mailersend/resources/identities.py @@ -197,4 +197,3 @@ def delete_identity_by_email( ) -AsyncIdentitiesResource = IdentitiesResource diff --git a/mailersend/resources/inbound.py b/mailersend/resources/inbound.py index 5e6f4fb..4a6bd6d 100644 --- a/mailersend/resources/inbound.py +++ b/mailersend/resources/inbound.py @@ -123,4 +123,3 @@ def delete(self, request: InboundDeleteRequest) -> APIResponse: return self._request(method="DELETE", path=f"inbound/{request.inbound_id}") -AsyncInboundResource = InboundResource diff --git a/mailersend/resources/messages.py b/mailersend/resources/messages.py index a790ad2..02997f3 100644 --- a/mailersend/resources/messages.py +++ b/mailersend/resources/messages.py @@ -53,4 +53,3 @@ def get_message(self, request: MessageGetRequest) -> APIResponse: return self._request(method="GET", path=f"messages/{request.message_id}") -AsyncMessages = Messages diff --git a/mailersend/resources/other.py b/mailersend/resources/other.py index 31bcc41..21e46c0 100644 --- a/mailersend/resources/other.py +++ b/mailersend/resources/other.py @@ -23,4 +23,3 @@ def get_quota(self) -> APIResponse: return self._request(method="GET", path="api-quota") -AsyncOther = Other diff --git a/mailersend/resources/recipients.py b/mailersend/resources/recipients.py index e122165..2412c66 100644 --- a/mailersend/resources/recipients.py +++ b/mailersend/resources/recipients.py @@ -390,4 +390,3 @@ def delete_from_on_hold(self, request: SuppressionDeleteRequest) -> APIResponse: ) -AsyncRecipients = Recipients diff --git a/mailersend/resources/schedules.py b/mailersend/resources/schedules.py index 8dd0544..158a644 100644 --- a/mailersend/resources/schedules.py +++ b/mailersend/resources/schedules.py @@ -81,4 +81,3 @@ def delete_schedule(self, request: ScheduleDeleteRequest) -> APIResponse: ) -AsyncSchedules = Schedules diff --git a/mailersend/resources/sms_activity.py b/mailersend/resources/sms_activity.py index 245b009..6822ae3 100644 --- a/mailersend/resources/sms_activity.py +++ b/mailersend/resources/sms_activity.py @@ -51,4 +51,3 @@ def get(self, request: SmsMessageGetRequest) -> APIResponse: ) -AsyncSmsActivity = SmsActivity diff --git a/mailersend/resources/sms_inbounds.py b/mailersend/resources/sms_inbounds.py index b9a3b3a..760b164 100644 --- a/mailersend/resources/sms_inbounds.py +++ b/mailersend/resources/sms_inbounds.py @@ -96,4 +96,3 @@ def delete_sms_inbound(self, request: SmsInboundDeleteRequest) -> APIResponse: ) -AsyncSmsInbounds = SmsInbounds diff --git a/mailersend/resources/sms_messages.py b/mailersend/resources/sms_messages.py index 75ed2e7..8f33f06 100644 --- a/mailersend/resources/sms_messages.py +++ b/mailersend/resources/sms_messages.py @@ -45,4 +45,3 @@ def get_sms_message(self, request: SmsMessageGetRequest) -> APIResponse: ) -AsyncSmsMessages = SmsMessages diff --git a/mailersend/resources/sms_numbers.py b/mailersend/resources/sms_numbers.py index 7b24844..b4935e0 100644 --- a/mailersend/resources/sms_numbers.py +++ b/mailersend/resources/sms_numbers.py @@ -88,4 +88,3 @@ def delete(self, request: SmsNumberDeleteRequest) -> APIResponse: ) -AsyncSmsNumbers = SmsNumbers diff --git a/mailersend/resources/sms_recipients.py b/mailersend/resources/sms_recipients.py index 03ff95a..d08e0d0 100644 --- a/mailersend/resources/sms_recipients.py +++ b/mailersend/resources/sms_recipients.py @@ -69,4 +69,3 @@ def update_sms_recipient(self, request: SmsRecipientUpdateRequest) -> APIRespons ) -AsyncSmsRecipients = SmsRecipients diff --git a/mailersend/resources/sms_sending.py b/mailersend/resources/sms_sending.py index 826403a..9a71f87 100644 --- a/mailersend/resources/sms_sending.py +++ b/mailersend/resources/sms_sending.py @@ -30,4 +30,3 @@ def send(self, request: SmsSendRequest) -> APIResponse: return self._request(method="POST", path="sms", body=payload) -AsyncSmsSending = SmsSending diff --git a/mailersend/resources/sms_webhooks.py b/mailersend/resources/sms_webhooks.py index 855ad2a..de9d53c 100644 --- a/mailersend/resources/sms_webhooks.py +++ b/mailersend/resources/sms_webhooks.py @@ -103,4 +103,3 @@ def delete_sms_webhook(self, request: SmsWebhookDeleteRequest) -> APIResponse: ) -AsyncSmsWebhooks = SmsWebhooks diff --git a/mailersend/resources/smtp_users.py b/mailersend/resources/smtp_users.py index 37d0c85..4e307e9 100644 --- a/mailersend/resources/smtp_users.py +++ b/mailersend/resources/smtp_users.py @@ -124,4 +124,3 @@ def delete_smtp_user(self, request: SmtpUserDeleteRequest) -> APIResponse: ) -AsyncSmtpUsers = SmtpUsers diff --git a/mailersend/resources/templates.py b/mailersend/resources/templates.py index e467430..4e8301d 100644 --- a/mailersend/resources/templates.py +++ b/mailersend/resources/templates.py @@ -83,4 +83,3 @@ def delete_template(self, request: TemplateDeleteRequest) -> APIResponse: ) -AsyncTemplates = Templates diff --git a/mailersend/resources/tokens.py b/mailersend/resources/tokens.py index 456e396..28e4fce 100644 --- a/mailersend/resources/tokens.py +++ b/mailersend/resources/tokens.py @@ -115,4 +115,3 @@ def delete_token(self, request: TokenDeleteRequest) -> APIResponse: return self._request(method="DELETE", path=f"token/{request.token_id}") -AsyncTokens = Tokens diff --git a/mailersend/resources/users.py b/mailersend/resources/users.py index 18a99c6..525fa36 100644 --- a/mailersend/resources/users.py +++ b/mailersend/resources/users.py @@ -165,4 +165,3 @@ def cancel_invite(self, request: InviteCancelRequest) -> APIResponse: return self._request(method="DELETE", path=f"invites/{request.invite_id}", data=lambda r: None) -AsyncUsers = Users diff --git a/mailersend/resources/webhooks.py b/mailersend/resources/webhooks.py index 1e874af..3454ab9 100644 --- a/mailersend/resources/webhooks.py +++ b/mailersend/resources/webhooks.py @@ -110,4 +110,3 @@ def delete_webhook(self, request: WebhookDeleteRequest) -> APIResponse: ) -AsyncWebhooks = Webhooks diff --git a/tests/unit/test_async_activity_resource.py b/tests/unit/test_async_activity_resource.py index 0ddc8bc..3b6dc05 100644 --- a/tests/unit/test_async_activity_resource.py +++ b/tests/unit/test_async_activity_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncActivity resource.""" +"""Tests for Activity resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.activity import AsyncActivity +from mailersend.resources.activity import Activity from mailersend.models.activity import ( ActivityRequest, ActivityQueryParams, @@ -21,10 +21,10 @@ def _make_mock_client(): return client -class TestAsyncActivity: +class TestActivity: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncActivity(self.mock_client) + self.resource = Activity(self.mock_client) async def test_get_returns_api_response(self): request = ActivityRequest( diff --git a/tests/unit/test_async_analytics_resource.py b/tests/unit/test_async_analytics_resource.py index c9ae5c1..3f66ecc 100644 --- a/tests/unit/test_async_analytics_resource.py +++ b/tests/unit/test_async_analytics_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncAnalytics resource.""" +"""Tests for Analytics resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.analytics import AsyncAnalytics +from mailersend.resources.analytics import Analytics from mailersend.models.analytics import AnalyticsRequest from mailersend.models.base import APIResponse @@ -26,10 +26,10 @@ def _make_request(): ) -class TestAsyncAnalytics: +class TestAnalytics: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncAnalytics(self.mock_client) + self.resource = Analytics(self.mock_client) async def test_get_activity_by_date_returns_api_response(self): result = await self.resource.get_activity_by_date(_make_request()) diff --git a/tests/unit/test_async_dmarc_monitoring_resource.py b/tests/unit/test_async_dmarc_monitoring_resource.py index 84cfb40..33444e7 100644 --- a/tests/unit/test_async_dmarc_monitoring_resource.py +++ b/tests/unit/test_async_dmarc_monitoring_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncDmarcMonitoring resource.""" +"""Tests for DmarcMonitoring resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.dmarc_monitoring import AsyncDmarcMonitoring +from mailersend.resources.dmarc_monitoring import DmarcMonitoring from mailersend.models.base import APIResponse from mailersend.models.dmarc_monitoring import ( DmarcMonitoringListRequest, @@ -28,10 +28,10 @@ def _make_mock_client(): return client -class TestAsyncDmarcMonitoring: +class TestDmarcMonitoring: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncDmarcMonitoring(self.mock_client) + self.resource = DmarcMonitoring(self.mock_client) async def test_list_monitors_returns_api_response(self): result = await self.resource.list_monitors() diff --git a/tests/unit/test_async_domains_resource.py b/tests/unit/test_async_domains_resource.py index 0313fd3..53dcea4 100644 --- a/tests/unit/test_async_domains_resource.py +++ b/tests/unit/test_async_domains_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncDomains resource.""" +"""Tests for Domains resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.domains import AsyncDomains +from mailersend.resources.domains import Domains from mailersend.models.domains import ( DomainListRequest, DomainListQueryParams, @@ -28,10 +28,10 @@ def _make_mock_client(): return client -class TestAsyncDomains: +class TestDomains: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncDomains(self.mock_client) + self.resource = Domains(self.mock_client) async def test_list_domains_returns_api_response(self): result = await self.resource.list_domains() diff --git a/tests/unit/test_async_email_resource.py b/tests/unit/test_async_email_resource.py index 73e199c..2507e26 100644 --- a/tests/unit/test_async_email_resource.py +++ b/tests/unit/test_async_email_resource.py @@ -1,9 +1,9 @@ -"""Tests for AsyncEmail resource.""" +"""Tests for Email resource.""" import pytest from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.email import AsyncEmail +from mailersend.resources.email import Email from mailersend.models.base import APIResponse from mailersend.models.email import EmailRequest, EmailContact @@ -32,10 +32,10 @@ def _make_email_request(): ) -class TestAsyncEmail: +class TestEmail: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncEmail(self.mock_client) + self.resource = Email(self.mock_client) async def test_send_returns_api_response(self): self.mock_client.request.return_value = _make_mock_response() diff --git a/tests/unit/test_async_email_verification_resource.py b/tests/unit/test_async_email_verification_resource.py index 491ac52..82f59a8 100644 --- a/tests/unit/test_async_email_verification_resource.py +++ b/tests/unit/test_async_email_verification_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncEmailVerification resource.""" +"""Tests for EmailVerification resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.email_verification import AsyncEmailVerification +from mailersend.resources.email_verification import EmailVerification from mailersend.models.base import APIResponse from mailersend.models.email_verification import ( EmailVerifyRequest, @@ -28,10 +28,10 @@ def _make_mock_client(): return client -class TestAsyncEmailVerification: +class TestEmailVerification: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncEmailVerification(self.mock_client) + self.resource = EmailVerification(self.mock_client) async def test_verify_email_returns_api_response(self): result = await self.resource.verify_email( diff --git a/tests/unit/test_async_identities_resource.py b/tests/unit/test_async_identities_resource.py index 875301d..0af190d 100644 --- a/tests/unit/test_async_identities_resource.py +++ b/tests/unit/test_async_identities_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncIdentitiesResource.""" +"""Tests for IdentitiesResource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.identities import AsyncIdentitiesResource +from mailersend.resources.identities import IdentitiesResource from mailersend.models.base import APIResponse from mailersend.models.identities import ( IdentityListRequest, @@ -27,10 +27,10 @@ def _make_mock_client(): return client -class TestAsyncIdentitiesResource: +class TestIdentitiesResource: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncIdentitiesResource(self.mock_client) + self.resource = IdentitiesResource(self.mock_client) async def test_list_identities_returns_api_response(self): request = IdentityListRequest(query_params=IdentityListQueryParams()) diff --git a/tests/unit/test_async_inbound_resource.py b/tests/unit/test_async_inbound_resource.py index b8a679c..6a60b9e 100644 --- a/tests/unit/test_async_inbound_resource.py +++ b/tests/unit/test_async_inbound_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncInboundResource.""" +"""Tests for InboundResource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.inbound import AsyncInboundResource +from mailersend.resources.inbound import InboundResource from mailersend.models.base import APIResponse from mailersend.models.inbound import ( InboundListRequest, @@ -34,10 +34,10 @@ def _make_forward(): return InboundForward(type="email", value="forward@example.com") -class TestAsyncInboundResource: +class TestInboundResource: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncInboundResource(self.mock_client) + self.resource = InboundResource(self.mock_client) async def test_list_returns_api_response(self): request = InboundListRequest(query_params=InboundListQueryParams()) diff --git a/tests/unit/test_async_messages_resource.py b/tests/unit/test_async_messages_resource.py index 29820b2..5476141 100644 --- a/tests/unit/test_async_messages_resource.py +++ b/tests/unit/test_async_messages_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncMessages resource.""" +"""Tests for Messages resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.messages import AsyncMessages +from mailersend.resources.messages import Messages from mailersend.models.base import APIResponse from mailersend.models.messages import ( MessagesListRequest, @@ -21,10 +21,10 @@ def _make_mock_client(): return client -class TestAsyncMessages: +class TestMessages: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncMessages(self.mock_client) + self.resource = Messages(self.mock_client) async def test_list_messages_returns_api_response(self): request = MessagesListRequest(query_params=MessagesListQueryParams()) diff --git a/tests/unit/test_async_other_resource.py b/tests/unit/test_async_other_resource.py index a00f5db..929b7a2 100644 --- a/tests/unit/test_async_other_resource.py +++ b/tests/unit/test_async_other_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncOther resource.""" +"""Tests for Other resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.other import AsyncOther +from mailersend.resources.other import Other from mailersend.models.base import APIResponse @@ -16,10 +16,10 @@ def _make_mock_client(): return client -class TestAsyncOther: +class TestOther: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncOther(self.mock_client) + self.resource = Other(self.mock_client) async def test_get_quota_returns_api_response(self): result = await self.resource.get_quota() diff --git a/tests/unit/test_async_recipients_resource.py b/tests/unit/test_async_recipients_resource.py index e0785e5..c4d65ce 100644 --- a/tests/unit/test_async_recipients_resource.py +++ b/tests/unit/test_async_recipients_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncRecipients resource.""" +"""Tests for Recipients resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.recipients import AsyncRecipients +from mailersend.resources.recipients import Recipients from mailersend.models.base import APIResponse from mailersend.models.recipients import ( RecipientsListRequest, @@ -26,10 +26,10 @@ def _make_mock_client(): return client -class TestAsyncRecipients: +class TestRecipients: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncRecipients(self.mock_client) + self.resource = Recipients(self.mock_client) async def test_list_recipients_returns_api_response(self): result = await self.resource.list_recipients() diff --git a/tests/unit/test_async_schedules_resource.py b/tests/unit/test_async_schedules_resource.py index 23c30be..6be3e6e 100644 --- a/tests/unit/test_async_schedules_resource.py +++ b/tests/unit/test_async_schedules_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncSchedules resource.""" +"""Tests for Schedules resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.schedules import AsyncSchedules +from mailersend.resources.schedules import Schedules from mailersend.models.base import APIResponse from mailersend.models.schedules import ( SchedulesListRequest, @@ -22,10 +22,10 @@ def _make_mock_client(): return client -class TestAsyncSchedules: +class TestSchedules: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncSchedules(self.mock_client) + self.resource = Schedules(self.mock_client) async def test_list_schedules_returns_api_response(self): request = SchedulesListRequest(query_params=SchedulesListQueryParams()) diff --git a/tests/unit/test_async_sms_activity_resource.py b/tests/unit/test_async_sms_activity_resource.py index 7cf6ca9..e63751a 100644 --- a/tests/unit/test_async_sms_activity_resource.py +++ b/tests/unit/test_async_sms_activity_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncSmsActivity resource.""" +"""Tests for SmsActivity resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.sms_activity import AsyncSmsActivity +from mailersend.resources.sms_activity import SmsActivity from mailersend.models.base import APIResponse from mailersend.models.sms_activity import ( SmsActivityListRequest, @@ -20,10 +20,10 @@ def _make_mock_client(): return client -class TestAsyncSmsActivity: +class TestSmsActivity: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncSmsActivity(self.mock_client) + self.resource = SmsActivity(self.mock_client) async def test_list_returns_api_response(self): result = await self.resource.list(SmsActivityListRequest()) diff --git a/tests/unit/test_async_sms_inbounds_resource.py b/tests/unit/test_async_sms_inbounds_resource.py index 801a720..66d1f29 100644 --- a/tests/unit/test_async_sms_inbounds_resource.py +++ b/tests/unit/test_async_sms_inbounds_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncSmsInbounds resource.""" +"""Tests for SmsInbounds resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.sms_inbounds import AsyncSmsInbounds +from mailersend.resources.sms_inbounds import SmsInbounds from mailersend.models.base import APIResponse from mailersend.models.sms_inbounds import ( SmsInboundsListRequest, @@ -24,10 +24,10 @@ def _make_mock_client(): return client -class TestAsyncSmsInbounds: +class TestSmsInbounds: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncSmsInbounds(self.mock_client) + self.resource = SmsInbounds(self.mock_client) async def test_list_sms_inbounds_returns_api_response(self): result = await self.resource.list_sms_inbounds(SmsInboundsListRequest()) diff --git a/tests/unit/test_async_sms_messages_resource.py b/tests/unit/test_async_sms_messages_resource.py index 488c0b4..b301ae5 100644 --- a/tests/unit/test_async_sms_messages_resource.py +++ b/tests/unit/test_async_sms_messages_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncSmsMessages resource.""" +"""Tests for SmsMessages resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.sms_messages import AsyncSmsMessages +from mailersend.resources.sms_messages import SmsMessages from mailersend.models.base import APIResponse from mailersend.models.sms_messages import ( SmsMessagesListRequest, @@ -21,10 +21,10 @@ def _make_mock_client(): return client -class TestAsyncSmsMessages: +class TestSmsMessages: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncSmsMessages(self.mock_client) + self.resource = SmsMessages(self.mock_client) async def test_list_sms_messages_returns_api_response(self): result = await self.resource.list_sms_messages(SmsMessagesListRequest()) diff --git a/tests/unit/test_async_sms_numbers_resource.py b/tests/unit/test_async_sms_numbers_resource.py index 5cffa9c..2f43073 100644 --- a/tests/unit/test_async_sms_numbers_resource.py +++ b/tests/unit/test_async_sms_numbers_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncSmsNumbers resource.""" +"""Tests for SmsNumbers resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.sms_numbers import AsyncSmsNumbers +from mailersend.resources.sms_numbers import SmsNumbers from mailersend.models.base import APIResponse from mailersend.models.sms_numbers import ( SmsNumbersListRequest, @@ -22,10 +22,10 @@ def _make_mock_client(): return client -class TestAsyncSmsNumbers: +class TestSmsNumbers: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncSmsNumbers(self.mock_client) + self.resource = SmsNumbers(self.mock_client) async def test_list_returns_api_response(self): result = await self.resource.list(SmsNumbersListRequest()) diff --git a/tests/unit/test_async_sms_recipients_resource.py b/tests/unit/test_async_sms_recipients_resource.py index 42d4608..5de2f85 100644 --- a/tests/unit/test_async_sms_recipients_resource.py +++ b/tests/unit/test_async_sms_recipients_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncSmsRecipients resource.""" +"""Tests for SmsRecipients resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.sms_recipients import AsyncSmsRecipients +from mailersend.resources.sms_recipients import SmsRecipients from mailersend.models.base import APIResponse from mailersend.models.sms_recipients import ( SmsRecipientsListRequest, @@ -23,10 +23,10 @@ def _make_mock_client(): return client -class TestAsyncSmsRecipients: +class TestSmsRecipients: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncSmsRecipients(self.mock_client) + self.resource = SmsRecipients(self.mock_client) async def test_list_sms_recipients_returns_api_response(self): result = await self.resource.list_sms_recipients(SmsRecipientsListRequest()) diff --git a/tests/unit/test_async_sms_sending_resource.py b/tests/unit/test_async_sms_sending_resource.py index 2ae313c..100a633 100644 --- a/tests/unit/test_async_sms_sending_resource.py +++ b/tests/unit/test_async_sms_sending_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncSmsSending resource.""" +"""Tests for SmsSending resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.sms_sending import AsyncSmsSending +from mailersend.resources.sms_sending import SmsSending from mailersend.models.base import APIResponse from mailersend.models.sms_sending import SmsSendRequest @@ -17,10 +17,10 @@ def _make_mock_client(): return client -class TestAsyncSmsSending: +class TestSmsSending: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncSmsSending(self.mock_client) + self.resource = SmsSending(self.mock_client) async def test_send_returns_api_response(self): request = SmsSendRequest( diff --git a/tests/unit/test_async_sms_webhooks_resource.py b/tests/unit/test_async_sms_webhooks_resource.py index 8004c71..e757b43 100644 --- a/tests/unit/test_async_sms_webhooks_resource.py +++ b/tests/unit/test_async_sms_webhooks_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncSmsWebhooks resource.""" +"""Tests for SmsWebhooks resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.sms_webhooks import AsyncSmsWebhooks +from mailersend.resources.sms_webhooks import SmsWebhooks from mailersend.models.base import APIResponse from mailersend.models.sms_webhooks import ( SmsWebhooksListRequest, @@ -25,10 +25,10 @@ def _make_mock_client(): return client -class TestAsyncSmsWebhooks: +class TestSmsWebhooks: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncSmsWebhooks(self.mock_client) + self.resource = SmsWebhooks(self.mock_client) async def test_list_sms_webhooks_returns_api_response(self): request = SmsWebhooksListRequest( diff --git a/tests/unit/test_async_smtp_users_resource.py b/tests/unit/test_async_smtp_users_resource.py index 5502678..2f3152b 100644 --- a/tests/unit/test_async_smtp_users_resource.py +++ b/tests/unit/test_async_smtp_users_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncSmtpUsers resource.""" +"""Tests for SmtpUsers resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.smtp_users import AsyncSmtpUsers +from mailersend.resources.smtp_users import SmtpUsers from mailersend.models.base import APIResponse from mailersend.models.smtp_users import ( SmtpUsersListRequest, @@ -24,10 +24,10 @@ def _make_mock_client(): return client -class TestAsyncSmtpUsers: +class TestSmtpUsers: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncSmtpUsers(self.mock_client) + self.resource = SmtpUsers(self.mock_client) async def test_list_smtp_users_returns_api_response(self): request = SmtpUsersListRequest(domain_id="dom123") diff --git a/tests/unit/test_async_templates_resource.py b/tests/unit/test_async_templates_resource.py index 2680bfa..e5b8f42 100644 --- a/tests/unit/test_async_templates_resource.py +++ b/tests/unit/test_async_templates_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncTemplates resource.""" +"""Tests for Templates resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.templates import AsyncTemplates +from mailersend.resources.templates import Templates from mailersend.models.base import APIResponse from mailersend.models.templates import ( TemplatesListRequest, @@ -22,10 +22,10 @@ def _make_mock_client(): return client -class TestAsyncTemplates: +class TestTemplates: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncTemplates(self.mock_client) + self.resource = Templates(self.mock_client) async def test_list_templates_returns_api_response(self): result = await self.resource.list_templates() diff --git a/tests/unit/test_async_tokens_resource.py b/tests/unit/test_async_tokens_resource.py index 7ff5ba3..f6a1f14 100644 --- a/tests/unit/test_async_tokens_resource.py +++ b/tests/unit/test_async_tokens_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncTokens resource.""" +"""Tests for Tokens resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.tokens import AsyncTokens +from mailersend.resources.tokens import Tokens from mailersend.models.base import APIResponse from mailersend.models.tokens import ( TokensListRequest, @@ -25,10 +25,10 @@ def _make_mock_client(): return client -class TestAsyncTokens: +class TestTokens: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncTokens(self.mock_client) + self.resource = Tokens(self.mock_client) async def test_list_tokens_returns_api_response(self): result = await self.resource.list_tokens(TokensListRequest()) diff --git a/tests/unit/test_async_users_resource.py b/tests/unit/test_async_users_resource.py index 5e77a2b..f4ec761 100644 --- a/tests/unit/test_async_users_resource.py +++ b/tests/unit/test_async_users_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncUsers resource.""" +"""Tests for Users resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.users import AsyncUsers +from mailersend.resources.users import Users from mailersend.models.base import APIResponse from mailersend.models.users import ( UsersListRequest, @@ -29,10 +29,10 @@ def _make_mock_client(): return client -class TestAsyncUsers: +class TestUsers: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncUsers(self.mock_client) + self.resource = Users(self.mock_client) async def test_list_users_returns_api_response(self): result = await self.resource.list_users(UsersListRequest()) diff --git a/tests/unit/test_async_webhooks_resource.py b/tests/unit/test_async_webhooks_resource.py index da82238..75c2935 100644 --- a/tests/unit/test_async_webhooks_resource.py +++ b/tests/unit/test_async_webhooks_resource.py @@ -1,8 +1,8 @@ -"""Tests for AsyncWebhooks resource.""" +"""Tests for Webhooks resource.""" from unittest.mock import AsyncMock, MagicMock -from mailersend.resources.webhooks import AsyncWebhooks +from mailersend.resources.webhooks import Webhooks from mailersend.models.base import APIResponse from mailersend.models.webhooks import ( WebhooksListRequest, @@ -24,10 +24,10 @@ def _make_mock_client(): return client -class TestAsyncWebhooks: +class TestWebhooks: def setup_method(self): self.mock_client = _make_mock_client() - self.resource = AsyncWebhooks(self.mock_client) + self.resource = Webhooks(self.mock_client) async def test_list_webhooks_returns_api_response(self): request = WebhooksListRequest( From ca5b5b7116c67f0420ee9c1113a1c1c39d8a061e Mon Sep 17 00:00:00 2001 From: rocribera Date: Wed, 3 Jun 2026 15:37:25 +0200 Subject: [PATCH 4/5] feat: set dual tests for sync and async --- mailersend/__init__.py | 6 +- mailersend/async_client.py | 171 +---- mailersend/base_client.py | 178 +++++ mailersend/client.py | 179 +---- tests/unit/test_activity_resource.py | 213 ++---- tests/unit/test_analytics_resource.py | 355 +++------- tests/unit/test_async_activity_resource.py | 67 -- tests/unit/test_async_analytics_resource.py | 87 --- tests/unit/test_async_client.py | 151 ++++- .../test_async_dmarc_monitoring_resource.py | 153 ----- tests/unit/test_async_domains_resource.py | 148 ----- .../test_async_email_verification_resource.py | 155 ----- tests/unit/test_async_identities_resource.py | 141 ---- tests/unit/test_async_inbound_resource.py | 124 ---- tests/unit/test_async_messages_resource.py | 58 -- tests/unit/test_async_other_resource.py | 32 - tests/unit/test_async_recipients_resource.py | 282 -------- tests/unit/test_async_schedules_resource.py | 64 -- .../unit/test_async_sms_activity_resource.py | 46 -- .../unit/test_async_sms_inbounds_resource.py | 100 --- .../unit/test_async_sms_messages_resource.py | 60 -- tests/unit/test_async_sms_numbers_resource.py | 74 --- .../test_async_sms_recipients_resource.py | 77 --- tests/unit/test_async_sms_sending_resource.py | 43 -- .../unit/test_async_sms_webhooks_resource.py | 109 ---- tests/unit/test_async_smtp_users_resource.py | 94 --- tests/unit/test_async_templates_resource.py | 72 -- tests/unit/test_async_tokens_resource.py | 105 --- tests/unit/test_async_users_resource.py | 133 ---- tests/unit/test_async_webhooks_resource.py | 103 --- tests/unit/test_client.py | 199 +++++- tests/unit/test_dmarc_monitoring_resource.py | 616 ++++-------------- tests/unit/test_domains_resource.py | 321 ++++----- ...ail_resource.py => test_email_resource.py} | 45 +- .../unit/test_email_verification_resource.py | 389 ++++------- tests/unit/test_identities_resource.py | 426 ++++-------- tests/unit/test_inbound_resource.py | 361 +++------- tests/unit/test_messages_resource.py | 185 ++---- tests/unit/test_other_resource.py | 47 ++ tests/unit/test_recipients_resource.py | 553 +++++++--------- tests/unit/test_schedules_resource.py | 235 ++----- tests/unit/test_sms_activity_resource.py | 135 ++-- tests/unit/test_sms_inbounds_resource.py | 217 +++--- tests/unit/test_sms_messages_resource.py | 116 ++-- tests/unit/test_sms_numbers_resource.py | 173 ++--- tests/unit/test_sms_recipients_resource.py | 143 ++-- tests/unit/test_sms_sending_resource.py | 118 ++-- tests/unit/test_sms_webhooks_resource.py | 193 +++--- tests/unit/test_smtp_users_resource.py | 301 +++------ tests/unit/test_templates_resource.py | 192 ++---- tests/unit/test_tokens_resource.py | 319 +++------ tests/unit/test_users_resource.py | 298 ++++----- tests/unit/test_webhooks_resource.py | 303 +++------ 53 files changed, 2645 insertions(+), 6820 deletions(-) create mode 100644 mailersend/base_client.py delete mode 100644 tests/unit/test_async_activity_resource.py delete mode 100644 tests/unit/test_async_analytics_resource.py delete mode 100644 tests/unit/test_async_dmarc_monitoring_resource.py delete mode 100644 tests/unit/test_async_domains_resource.py delete mode 100644 tests/unit/test_async_email_verification_resource.py delete mode 100644 tests/unit/test_async_identities_resource.py delete mode 100644 tests/unit/test_async_inbound_resource.py delete mode 100644 tests/unit/test_async_messages_resource.py delete mode 100644 tests/unit/test_async_other_resource.py delete mode 100644 tests/unit/test_async_recipients_resource.py delete mode 100644 tests/unit/test_async_schedules_resource.py delete mode 100644 tests/unit/test_async_sms_activity_resource.py delete mode 100644 tests/unit/test_async_sms_inbounds_resource.py delete mode 100644 tests/unit/test_async_sms_messages_resource.py delete mode 100644 tests/unit/test_async_sms_numbers_resource.py delete mode 100644 tests/unit/test_async_sms_recipients_resource.py delete mode 100644 tests/unit/test_async_sms_sending_resource.py delete mode 100644 tests/unit/test_async_sms_webhooks_resource.py delete mode 100644 tests/unit/test_async_smtp_users_resource.py delete mode 100644 tests/unit/test_async_templates_resource.py delete mode 100644 tests/unit/test_async_tokens_resource.py delete mode 100644 tests/unit/test_async_users_resource.py delete mode 100644 tests/unit/test_async_webhooks_resource.py rename tests/unit/{test_async_email_resource.py => test_email_resource.py} (65%) create mode 100644 tests/unit/test_other_resource.py diff --git a/mailersend/__init__.py b/mailersend/__init__.py index 1ec9e83..9a79ac3 100644 --- a/mailersend/__init__.py +++ b/mailersend/__init__.py @@ -5,7 +5,11 @@ """ from .client import MailerSendClient -from .async_client import AsyncMailerSendClient + +try: + from .async_client import AsyncMailerSendClient +except ImportError: + AsyncMailerSendClient = None # type: ignore[assignment,misc] # Import all builders for better UX - users can import everything from main module from .builders.email import EmailBuilder diff --git a/mailersend/async_client.py b/mailersend/async_client.py index e806f64..60d305a 100644 --- a/mailersend/async_client.py +++ b/mailersend/async_client.py @@ -1,11 +1,11 @@ import asyncio import logging -import os from typing import Any, Dict, Optional from urllib.parse import urljoin import httpx +from .base_client import _BaseMailerSendClient, RETRY_STATUSES from .constants import DEFAULT_BASE_URL, DEFAULT_TIMEOUT, USER_AGENT from .exceptions import ( AuthenticationError, @@ -15,36 +15,9 @@ ResourceNotFoundError, ServerError, ) -from .resources.activity import Activity -from .resources.analytics import Analytics -from .resources.dmarc_monitoring import DmarcMonitoring -from .resources.domains import Domains -from .resources.email import Email -from .resources.email_verification import EmailVerification -from .resources.identities import IdentitiesResource -from .resources.inbound import InboundResource -from .resources.messages import Messages -from .resources.other import Other -from .resources.recipients import Recipients -from .resources.schedules import Schedules -from .resources.sms_activity import SmsActivity -from .resources.sms_inbounds import SmsInbounds -from .resources.sms_messages import SmsMessages -from .resources.sms_numbers import SmsNumbers -from .resources.sms_recipients import SmsRecipients -from .resources.sms_sending import SmsSending -from .resources.sms_webhooks import SmsWebhooks -from .resources.smtp_users import SmtpUsers -from .resources.templates import Templates -from .resources.tokens import Tokens -from .resources.users import Users -from .resources.webhooks import Webhooks -from .logging import get_logger, RequestLogger -_RETRY_STATUSES = frozenset([429, 500, 502, 503, 504]) - -class AsyncMailerSendClient: +class AsyncMailerSendClient(_BaseMailerSendClient): """ Async client for the MailerSend API. @@ -56,8 +29,10 @@ class AsyncMailerSendClient: >>> async with AsyncMailerSendClient() as client: ... response = await client.emails.send(email_request) - >>> # Using explicit API key + >>> # Using explicit API key (remember to close when done) >>> client = AsyncMailerSendClient(api_key="your_api_key") + >>> response = await client.emails.send(email_request) + >>> await client.close() >>> # Enable debug logging for detailed request/response info >>> client = AsyncMailerSendClient(debug=True) @@ -88,21 +63,7 @@ def __init__( ValueError: If no API key is provided and MAILERSEND_API_KEY environment variable is not set """ - resolved_api_key = api_key or os.getenv("MAILERSEND_API_KEY") - - if not resolved_api_key: - raise ValueError( - "API key is required. Either pass it as 'api_key' parameter or " - "set the 'MAILERSEND_API_KEY' environment variable." - ) - - self.api_key = resolved_api_key - self.base_url = base_url - self.timeout = timeout - self.max_retries = max_retries - self.debug = debug - self.logger = logger or get_logger(debug=debug) - self.request_logger = RequestLogger(self.logger) + super().__init__(api_key, base_url, timeout, max_retries, debug, logger) self._client = httpx.AsyncClient( headers={ @@ -114,55 +75,7 @@ def __init__( timeout=self.timeout, ) - self.emails = Email(self) - self.activities = Activity(self) - self.analytics = Analytics(self) - self.domains = Domains(self) - self.identities = IdentitiesResource(self) - self.inbound = InboundResource(self) - self.templates = Templates(self) - self.tokens = Tokens(self) - self.webhooks = Webhooks(self) - self.email_verification = EmailVerification(self) - self.users = Users(self) - self.messages = Messages(self) - self.recipients = Recipients(self) - self.schedules = Schedules(self) - self.sms_messages = SmsMessages(self) - self.smtp_users = SmtpUsers(self) - self.sms_sending = SmsSending(self) - self.sms_numbers = SmsNumbers(self) - self.sms_activity = SmsActivity(self) - self.sms_inbounds = SmsInbounds(self) - self.sms_recipients = SmsRecipients(self) - self.sms_webhooks = SmsWebhooks(self) - self.api_quota = Other(self) - self.dmarc_monitoring = DmarcMonitoring(self) - - self.logger.info("AsyncMailerSendClient initialized successfully") - - def enable_debug(self) -> None: - """Enable debug logging for this client instance.""" - self.debug = True - self.logger.setLevel(logging.DEBUG) - self.logger.info("Debug mode enabled") - - def disable_debug(self) -> None: - """Disable debug logging for this client instance.""" - self.debug = False - self.logger.setLevel(logging.WARNING) - self.logger.info("Debug mode disabled") - - def get_debug_info(self) -> Dict[str, Any]: - """Get current debug and configuration information.""" - return { - "debug_enabled": self.debug, - "base_url": self.base_url, - "timeout": self.timeout, - "max_retries": self.max_retries, - "user_agent": USER_AGENT, - "logger_level": self.logger.level, - } + self.logger.info(f"{self.__class__.__name__} initialized successfully") async def request( self, @@ -194,8 +107,6 @@ async def request( url = urljoin(self.base_url, path) request_id = self.request_logger.start_request(method, url, params, body) - last_exception: Optional[Exception] = None - for attempt in range(self.max_retries + 1): try: response = await self._client.request( @@ -210,47 +121,30 @@ async def request( if 200 <= response.status_code < 300: return response - # Retry on transient errors (except on the last attempt) if ( - response.status_code in _RETRY_STATUSES + response.status_code in RETRY_STATUSES and attempt < self.max_retries ): if response.status_code == 429: retry_after = response.headers.get("retry-after") - delay = ( - float(retry_after) if retry_after else 0.3 * (2**attempt) - ) + try: + delay = ( + float(retry_after) + if retry_after + else 0.3 * (2**attempt) + ) + except ValueError: + delay = 0.3 * (2**attempt) else: delay = 0.3 * (2**attempt) self.request_logger.log_retry(attempt + 1, delay) await asyncio.sleep(delay) continue - error_message = self._get_error_message(response) - self.logger.error( - f"API error {response.status_code}: {error_message}", - extra={"request_id": request_id}, + self._raise_for_status( + response, self._get_error_message(response), request_id ) - if response.status_code == 401: - raise AuthenticationError(error_message, response) - elif response.status_code == 404: - raise ResourceNotFoundError(error_message, response) - elif response.status_code == 429: - retry_after = response.headers.get("retry-after") - remaining = response.headers.get("x-apiquota-remaining") - self.logger.warning( - f"Rate limit exceeded. Retry after: {retry_after}s, Remaining: {remaining}", - extra={"request_id": request_id}, - ) - raise RateLimitExceeded(error_message, response) - elif 400 <= response.status_code < 500: - raise BadRequestError(error_message, response) - elif 500 <= response.status_code < 600: - raise ServerError(error_message, response) - else: - raise MailerSendError(error_message, response) - except ( AuthenticationError, ResourceNotFoundError, @@ -261,7 +155,6 @@ async def request( ): raise except httpx.RequestError as e: - last_exception = e if attempt < self.max_retries: delay = 0.3 * (2**attempt) self.request_logger.log_retry(attempt + 1, delay) @@ -270,27 +163,6 @@ async def request( self.request_logger.log_error(e) raise MailerSendError(f"Request failed: {str(e)}") from e - # Should not be reached, but satisfies type checker - if last_exception: - raise MailerSendError(f"Request failed: {str(last_exception)}") - raise MailerSendError("Request failed after retries") - - def _get_error_message(self, response: httpx.Response) -> str: - try: - error_data = response.json() - if isinstance(error_data, dict): - message = error_data.get("message", "Unknown error") - errors = error_data.get("errors", {}) - if errors: - error_details = "; ".join( - f"{key}: {', '.join(msgs)}" for key, msgs in errors.items() - ) - return f"{message}: {error_details}" - return message - except Exception: - pass - return f"Error {response.status_code}: {response.text}" - async def close(self) -> None: """Close the underlying httpx client and release resources.""" await self._client.aclose() @@ -298,5 +170,10 @@ async def close(self) -> None: async def __aenter__(self) -> "AsyncMailerSendClient": return self - async def __aexit__(self, *_: Any) -> None: + async def __aexit__( + self, + exc_type: Optional[type], + exc_val: Optional[BaseException], + exc_tb: Optional[Any], + ) -> None: await self.close() diff --git a/mailersend/base_client.py b/mailersend/base_client.py new file mode 100644 index 0000000..c22d215 --- /dev/null +++ b/mailersend/base_client.py @@ -0,0 +1,178 @@ +"""Shared base client for MailerSendClient and AsyncMailerSendClient.""" + +import logging +import os +from typing import Any, Dict, NoReturn, Optional + +from .constants import DEFAULT_BASE_URL, DEFAULT_TIMEOUT, USER_AGENT +from .exceptions import ( + AuthenticationError, + BadRequestError, + MailerSendError, + RateLimitExceeded, + ResourceNotFoundError, + ServerError, +) +from .logging import get_logger, RequestLogger +from .resources.activity import Activity +from .resources.analytics import Analytics +from .resources.dmarc_monitoring import DmarcMonitoring +from .resources.domains import Domains +from .resources.email import Email +from .resources.email_verification import EmailVerification +from .resources.identities import IdentitiesResource +from .resources.inbound import InboundResource +from .resources.messages import Messages +from .resources.other import Other +from .resources.recipients import Recipients +from .resources.schedules import Schedules +from .resources.sms_activity import SmsActivity +from .resources.sms_inbounds import SmsInbounds +from .resources.sms_messages import SmsMessages +from .resources.sms_numbers import SmsNumbers +from .resources.sms_recipients import SmsRecipients +from .resources.sms_sending import SmsSending +from .resources.sms_webhooks import SmsWebhooks +from .resources.smtp_users import SmtpUsers +from .resources.templates import Templates +from .resources.tokens import Tokens +from .resources.users import Users +from .resources.webhooks import Webhooks + +# HTTP status codes that warrant a retry +RETRY_STATUSES: frozenset = frozenset([429, 500, 502, 503, 504]) + + +class _BaseMailerSendClient: + """ + Shared base for MailerSendClient and AsyncMailerSendClient. + + Handles API key resolution, resource initialisation, debug helpers, + and error parsing/dispatch. Subclasses provide the transport layer + (requests vs httpx) and the request() method (sync vs async). + """ + + def __init__( + self, + api_key: Optional[str] = None, + base_url: str = DEFAULT_BASE_URL, + timeout: int = DEFAULT_TIMEOUT, + max_retries: int = 3, + debug: bool = False, + logger: Optional[logging.Logger] = None, + ) -> None: + resolved_api_key = api_key or os.getenv("MAILERSEND_API_KEY") + if not resolved_api_key: + raise ValueError( + "API key is required. Either pass it as 'api_key' parameter or " + "set the 'MAILERSEND_API_KEY' environment variable." + ) + + self.api_key = resolved_api_key + self.base_url = base_url.rstrip("/") + "/" + self.timeout = timeout + self.max_retries = max_retries + self.debug = debug + self.logger = logger or get_logger(debug=debug) + self.request_logger = RequestLogger(self.logger) + + self._init_resources() + + def _init_resources(self) -> None: + """Instantiate all API resource objects.""" + self.emails = Email(self) + self.activities = Activity(self) + self.analytics = Analytics(self) + self.domains = Domains(self) + self.identities = IdentitiesResource(self) + self.inbound = InboundResource(self) + self.templates = Templates(self) + self.tokens = Tokens(self) + self.webhooks = Webhooks(self) + self.email_verification = EmailVerification(self) + self.users = Users(self) + self.messages = Messages(self) + self.recipients = Recipients(self) + self.schedules = Schedules(self) + self.sms_messages = SmsMessages(self) + self.smtp_users = SmtpUsers(self) + self.sms_sending = SmsSending(self) + self.sms_numbers = SmsNumbers(self) + self.sms_activity = SmsActivity(self) + self.sms_inbounds = SmsInbounds(self) + self.sms_recipients = SmsRecipients(self) + self.sms_webhooks = SmsWebhooks(self) + self.api_quota = Other(self) + self.dmarc_monitoring = DmarcMonitoring(self) + + @staticmethod + def _get_error_message(response: Any) -> str: + """Extract a human-readable error message from an HTTP response.""" + try: + error_data = response.json() + if isinstance(error_data, dict): + message = error_data.get("message", "Unknown error") + errors = error_data.get("errors", {}) + if errors: + error_details = "; ".join( + f"{key}: {', '.join(msgs)}" for key, msgs in errors.items() + ) + return f"{message}: {error_details}" + return message + except Exception: + pass + try: + return f"Error {response.status_code}: {response.text}" + except Exception: + return f"Error {response.status_code}: " + + def _raise_for_status( + self, response: Any, error_message: str, request_id: str + ) -> NoReturn: + """Log and raise the appropriate SDK exception for a non-2xx response.""" + self.logger.error( + f"API error {response.status_code}: {error_message}", + extra={"request_id": request_id}, + ) + if response.status_code == 401: + raise AuthenticationError(error_message, response) + elif response.status_code == 404: + raise ResourceNotFoundError(error_message, response) + elif response.status_code == 429: + retry_after = response.headers.get("retry-after") + remaining = response.headers.get("x-apiquota-remaining") + self.logger.warning( + f"Rate limit exceeded. Retry after: {retry_after}s, " + f"Remaining: {remaining}", + extra={"request_id": request_id}, + ) + raise RateLimitExceeded(error_message, response) + elif 400 <= response.status_code < 500: + raise BadRequestError(error_message, response) + elif 500 <= response.status_code < 600: + raise ServerError(error_message, response) + else: + raise MailerSendError(error_message, response) + + def enable_debug(self) -> None: + """Enable debug logging for this client instance.""" + self.debug = True + self.logger.setLevel(logging.DEBUG) + self.logger.info("Debug mode enabled") + + def disable_debug(self) -> None: + """Disable debug logging for this client instance.""" + self.debug = False + self.logger.setLevel(logging.WARNING) + self.logger.info("Debug mode disabled") + + def get_debug_info(self) -> Dict[str, Any]: + """Get current debug and configuration information.""" + return { + "debug_enabled": self.debug, + "base_url": self.base_url, + "timeout": self.timeout, + "max_retries": self.max_retries, + "user_agent": USER_AGENT, + "logger_level": self.logger.level, + } diff --git a/mailersend/client.py b/mailersend/client.py index 455c448..1ac5e06 100644 --- a/mailersend/client.py +++ b/mailersend/client.py @@ -1,49 +1,17 @@ import logging -import os -from typing import Optional, Dict, Any, Type, cast, Union +from typing import Any, Dict, Optional from urllib.parse import urljoin import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry +from .base_client import _BaseMailerSendClient, RETRY_STATUSES from .constants import DEFAULT_BASE_URL, DEFAULT_TIMEOUT, USER_AGENT -from .exceptions import ( - MailerSendError, - AuthenticationError, - RateLimitExceeded, - ResourceNotFoundError, - BadRequestError, - ServerError, -) -from .resources.email import Email -from .resources.activity import Activity -from .resources.analytics import Analytics -from .resources.domains import Domains -from .resources.identities import IdentitiesResource -from .resources.inbound import InboundResource -from .resources.templates import Templates -from .resources.tokens import Tokens -from .resources.webhooks import Webhooks -from .resources.email_verification import EmailVerification -from .resources.users import Users -from .resources.messages import Messages -from .resources.recipients import Recipients -from .resources.schedules import Schedules -from .resources.smtp_users import SmtpUsers -from .resources.sms_activity import SmsActivity -from .resources.sms_inbounds import SmsInbounds -from .resources.sms_messages import SmsMessages -from .resources.sms_numbers import SmsNumbers -from .resources.sms_recipients import SmsRecipients -from .resources.sms_sending import SmsSending -from .resources.sms_webhooks import SmsWebhooks -from .resources.other import Other -from .resources.dmarc_monitoring import DmarcMonitoring -from .logging import get_logger, RequestLogger +from .exceptions import MailerSendError -class MailerSendClient: +class MailerSendClient(_BaseMailerSendClient): """ Main client for the MailerSend API. @@ -58,8 +26,9 @@ class MailerSendClient: >>> # Using explicit API key >>> client = MailerSendClient(api_key="your_api_key") - >>> # Enable debug logging for detailed request/response info - >>> client = MailerSendClient(debug=True) + >>> # Use as a context manager to ensure the session is closed + >>> with MailerSendClient() as client: + ... response = client.emails.send(email_request) """ def __init__( @@ -87,35 +56,18 @@ def __init__( ValueError: If no API key is provided and MAILERSEND_API_KEY environment variable is not set """ - # Try to get API key from environment variable first, then from parameter - resolved_api_key = api_key or os.getenv("MAILERSEND_API_KEY") + super().__init__(api_key, base_url, timeout, max_retries, debug, logger) - if not resolved_api_key: - raise ValueError( - "API key is required. Either pass it as 'api_key' parameter or " - "set the 'MAILERSEND_API_KEY' environment variable." - ) - - self.api_key = resolved_api_key - self.base_url = base_url - self.timeout = timeout - self.debug = debug - self.logger = logger or get_logger(debug=debug) - self.request_logger = RequestLogger(self.logger) - - # Initialize session with retry logic self.session = requests.Session() retry_strategy = Retry( total=max_retries, backoff_factor=0.3, - status_forcelist=[429, 500, 502, 503, 504], + status_forcelist=sorted(RETRY_STATUSES), allowed_methods=["GET", "POST", "PUT", "DELETE", "PATCH"], ) adapter = HTTPAdapter(max_retries=retry_strategy) self.session.mount("https://", adapter) self.session.mount("http://", adapter) - - # Set default headers self.session.headers.update( { "Authorization": f"Bearer {self.api_key}", @@ -125,35 +77,9 @@ def __init__( } ) - # Initialize resources - self.emails = Email(self) - self.activities = Activity(self) - self.analytics = Analytics(self) - self.domains = Domains(self) - self.identities = IdentitiesResource(self) - self.inbound = InboundResource(self) - self.templates = Templates(self) - self.tokens = Tokens(self) - self.webhooks = Webhooks(self) - self.email_verification = EmailVerification(self) - self.users = Users(self) - self.messages = Messages(self) - self.recipients = Recipients(self) - self.schedules = Schedules(self) - self.sms_messages = SmsMessages(self) - self.smtp_users = SmtpUsers(self) - self.sms_sending = SmsSending(self) - self.sms_numbers = SmsNumbers(self) - self.sms_activity = SmsActivity(self) - self.sms_inbounds = SmsInbounds(self) - self.sms_recipients = SmsRecipients(self) - self.sms_webhooks = SmsWebhooks(self) - self.api_quota = Other(self) - self.dmarc_monitoring = DmarcMonitoring(self) - - self.logger.info("MailerSend client initialized successfully") + self.logger.info(f"{self.__class__.__name__} initialized successfully") if debug: - self.logger.info("🐛 Debug mode enabled - detailed logging active") + self.logger.info("Debug mode enabled") def request( self, @@ -183,92 +109,33 @@ def request( MailerSendError: For other API errors """ url = urljoin(self.base_url, path) - - # Start request logging request_id = self.request_logger.start_request(method, url, params, body) try: response = self.session.request( method=method, url=url, params=params, json=body, timeout=self.timeout ) - - # Log response details self.request_logger.log_response(response) - # Handle different response status codes if 200 <= response.status_code < 300: return response - # Handle error responses - error_message = self._get_error_message(response) - - # Log the error details before raising - self.logger.error( - f"API error {response.status_code}: {error_message}", - extra={"request_id": request_id}, + self._raise_for_status( + response, self._get_error_message(response), request_id ) - if response.status_code == 401: - raise AuthenticationError(error_message, response) - elif response.status_code == 404: - raise ResourceNotFoundError(error_message, response) - elif response.status_code == 429: - # Log rate limit details - retry_after = response.headers.get("retry-after") - remaining = response.headers.get("x-apiquota-remaining") - self.logger.warning( - f"⚠️ Rate limit exceeded. Retry after: {retry_after}s, Remaining: {remaining}", - extra={"request_id": request_id}, - ) - raise RateLimitExceeded(error_message, response) - elif 400 <= response.status_code < 500: - raise BadRequestError(error_message, response) - elif 500 <= response.status_code < 600: - raise ServerError(error_message, response) - else: - raise MailerSendError(error_message, response) - except requests.RequestException as e: self.request_logger.log_error(e) - raise MailerSendError(f"Request failed: {str(e)}") - - def _get_error_message(self, response: requests.Response) -> str: - """Extract error message from response.""" - try: - error_data = response.json() - if isinstance(error_data, dict): - message = error_data.get("message", "Unknown error") - errors = error_data.get("errors", {}) - if errors: - error_details = "; ".join( - f"{key}: {', '.join(msgs)}" for key, msgs in errors.items() - ) - return f"{message}: {error_details}" - return message - except Exception: - pass - - return f"Error {response.status_code}: {response.text}" - - def enable_debug(self): - """Enable debug logging for this client instance.""" - self.debug = True - self.logger.setLevel(logging.DEBUG) - self.logger.info("🐛 Debug mode enabled") - - def disable_debug(self): - """Disable debug logging for this client instance.""" - self.debug = False - self.logger.setLevel(logging.WARNING) - self.logger.info("Debug mode disabled") + raise MailerSendError(f"Request failed: {str(e)}") from e def get_debug_info(self) -> Dict[str, Any]: """Get current debug and configuration information.""" - return { - "debug_enabled": self.debug, - "base_url": self.base_url, - "timeout": self.timeout, - "user_agent": USER_AGENT, - "logger_level": self.logger.level, - "session_adapters": list(self.session.adapters.keys()), - } + info = super().get_debug_info() + info["session_adapters"] = list(self.session.adapters.keys()) + return info + + def __enter__(self) -> "MailerSendClient": + return self + + def __exit__(self, *_: Any) -> None: + self.session.close() diff --git a/tests/unit/test_activity_resource.py b/tests/unit/test_activity_resource.py index c1c2260..85c0002 100644 --- a/tests/unit/test_activity_resource.py +++ b/tests/unit/test_activity_resource.py @@ -1,6 +1,8 @@ +"""Tests for Activity resource.""" +import inspect + +from unittest.mock import AsyncMock, MagicMock, Mock import pytest -from unittest.mock import Mock, patch -from requests import Response from mailersend.resources.activity import Activity from mailersend.models.activity import ( @@ -9,159 +11,72 @@ SingleActivityRequest, ) from mailersend.models.base import APIResponse -from mailersend.exceptions import ValidationError - - -class TestActivityResource: - """Test the Activity resource class.""" - - @pytest.fixture - def activity_resource(self): - """Create an Activity resource instance with a mocked client.""" - mock_client = Mock() - return Activity(mock_client) - @pytest.fixture - def mock_response(self): - """Create a mock HTTP response.""" - response = Mock(spec=Response) - response.status_code = 200 - response.headers = {"Content-Type": "application/json"} - response.json.return_value = { - "data": { - "id": "5ee0b166b251345e407c9207", - "created_at": "2020-06-04 12:00:00", - "updated_at": "2020-06-04 12:00:00", - "type": "clicked", - "email": { - "id": "5ee0b166b251345e407c9201", - "from": "colleen.wiza@example.net", - "subject": "Magni aperiam sunt nam omnis.", - "text": "Lorem ipsum dolor sit amet, consectetuer adipiscin", - "html": " Date: Wed, 3 Jun 2026 16:40:47 +0200 Subject: [PATCH 5/5] feat: move async section in README --- README.md | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5d0881d..0e8e389 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ MailerSend Python SDK - [Builder Pattern](#builder-pattern) - [Resource Classes](#resource-classes) - [Request and Response Models](#request-and-response-models) + - [Async Support](#async-support) - [Response Data Access](#response-data-access) - [Multiple Access Patterns](#multiple-access-patterns) - [Dict-like Access](#dict-like-access) @@ -181,7 +182,7 @@ MailerSend Python SDK - [Remove IP from favorites](#remove-ip-from-favorites) - [Other Endpoints](#other-endpoints) - [Get API Quota](#get-api-quota) - - [Async Support](#async-support) + - [Async Usage](#async-usage) - [Basic Async Usage](#basic-async-usage) - [Concurrent Requests](#concurrent-requests) - [Async Error Handling](#async-error-handling) @@ -320,6 +321,32 @@ print(response.number) # Validated phone number print(response.created_at) # Validated datetime object ``` +## Async Support + +The SDK ships an async client built on [`httpx`](https://www.python-httpx.org/) for use in async applications (FastAPI, asyncio, etc.). It exposes the exact same resource namespaces and builder/model interfaces as the sync client. + +```python +from mailersend import AsyncMailerSendClient + +# Recommended — use as an async context manager +async with AsyncMailerSendClient() as client: + response = await client.emails.send(email_request) + print(response["id"]) +``` + +The async client accepts the same configuration parameters: + +```python +client = AsyncMailerSendClient( + api_key="your_api_key", # or set MAILERSEND_API_KEY env var + timeout=30, + max_retries=3, + debug=True, +) +``` + +Retries, rate-limit handling, and the error exception hierarchy (`AuthenticationError`, `RateLimitExceeded`, `ServerError`, etc.) behave identically to the sync client. + # Response Data Access @@ -2645,11 +2672,11 @@ ms = MailerSendClient() response = ms.api_quota.get_quota() ``` - + -## Async Support +## Async Usage -The SDK provides a fully async-compatible client, `AsyncMailerSendClient`, built on `httpx.AsyncClient`. It exposes the same resources and methods as the synchronous `MailerSendClient` — prefixed with `async`/`await` — so you can use it anywhere `asyncio` is available. +The `AsyncMailerSendClient` exposes the same resources and methods as the synchronous `MailerSendClient` — prefixed with `async`/`await` — so you can use it anywhere `asyncio` is available. ### Basic Async Usage