diff --git a/README.md b/README.md
index 1fa10cd..0e8e389 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,7 @@ MailerSend Python SDK
[](./LICENSE)
# Table of Contents
+
- [Table of Contents](#table-of-contents)
- [Installation](#installation)
- [Requirements](#requirements)
@@ -17,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)
@@ -119,20 +121,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)
@@ -194,6 +182,11 @@ MailerSend Python SDK
- [Remove IP from favorites](#remove-ip-from-favorites)
- [Other Endpoints](#other-endpoints)
- [Get API Quota](#get-api-quota)
+ - [Async Usage](#async-usage)
+ - [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 +205,7 @@ pip install mailersend
## Requirements
-- Python 3.7+
+- Python 3.10+
- An API Key from [mailersend.com](https://www.mailersend.com)
## Authentication
@@ -278,7 +271,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 +302,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
@@ -328,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
@@ -337,6 +356,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 +382,7 @@ if "error" in response:
```
### Attribute Access
+
Access data using dot notation for cleaner code:
```python
@@ -376,6 +397,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 +412,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 +436,7 @@ value_list = response.data_values
## Data Format Conversion
### Convert to Dictionary
+
Get the complete response as a dictionary:
```python
@@ -437,6 +461,7 @@ headers_only = response_dict["headers"]
```
### Convert to JSON
+
Get JSON string representation with various formatting options:
```python
@@ -455,6 +480,7 @@ json_string = json.dumps(response)
```
### Extract Raw Data
+
Access just the API response data without metadata:
```python
@@ -474,6 +500,7 @@ else:
## Headers and Metadata
### Access Response Headers
+
Headers can be accessed in multiple ways with automatic case handling:
```python
@@ -494,6 +521,7 @@ retry_after = response.headers.get("retry-after", "0")
```
### Response Metadata
+
Access useful metadata about the API response:
```python
@@ -518,6 +546,7 @@ if "meta" in response.data:
## Error Handling with Responses
### Check Response Status
+
Always check if the response was successful:
```python
@@ -528,33 +557,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 +605,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 +1573,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"])
@@ -1823,204 +1853,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
@@ -2050,7 +1882,7 @@ request = (SmsSendingBuilder()
"data": {"name": "John", "order_id": "12345"}
},
{
- "phone_number": "+1234567891",
+ "phone_number": "+1234567891",
"data": {"name": "Jane", "order_id": "12346"}
}
])
@@ -2840,6 +2672,164 @@ ms = MailerSendClient()
response = ms.api_quota.get_quota()
```
+
+
+## Async Usage
+
+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
+
+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_templates_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 +2850,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}")
```
@@ -2875,10 +2865,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
@@ -2919,36 +2911,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..9a79ac3 100644
--- a/mailersend/__init__.py
+++ b/mailersend/__init__.py
@@ -6,6 +6,11 @@
from .client import MailerSendClient
+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
from .builders.activity import ActivityBuilder, SingleActivityBuilder
@@ -65,8 +70,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..60d305a
--- /dev/null
+++ b/mailersend/async_client.py
@@ -0,0 +1,179 @@
+import asyncio
+import logging
+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,
+ BadRequestError,
+ MailerSendError,
+ RateLimitExceeded,
+ ResourceNotFoundError,
+ ServerError,
+)
+
+
+class AsyncMailerSendClient(_BaseMailerSendClient):
+ """
+ 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 (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)
+ """
+
+ 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
+ """
+ super().__init__(api_key, base_url, timeout, max_retries, debug, 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.logger.info(f"{self.__class__.__name__} initialized successfully")
+
+ 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)
+
+ 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
+
+ if (
+ response.status_code in RETRY_STATUSES
+ and attempt < self.max_retries
+ ):
+ if response.status_code == 429:
+ retry_after = response.headers.get("retry-after")
+ 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
+
+ self._raise_for_status(
+ response, self._get_error_message(response), request_id
+ )
+
+ except (
+ AuthenticationError,
+ ResourceNotFoundError,
+ RateLimitExceeded,
+ BadRequestError,
+ ServerError,
+ MailerSendError,
+ ):
+ raise
+ except httpx.RequestError as 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
+
+ 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,
+ 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/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..594aa84 100644
--- a/mailersend/resources/__init__.py
+++ b/mailersend/resources/__init__.py
@@ -26,6 +26,7 @@
from .sms_inbounds import SmsInbounds
from .other import Other
from .dmarc_monitoring import DmarcMonitoring
+from .smtp_users import SmtpUsers
__all__ = [
"BaseResource",
@@ -50,6 +51,7 @@
"SmsRecipients",
"SmsWebhooks",
"SmsInbounds",
+ "SmtpUsers",
"Other",
"DmarcMonitoring",
]
diff --git a/mailersend/resources/activity.py b/mailersend/resources/activity.py
index 45037bd..41e7475 100644
--- a/mailersend/resources/activity.py
+++ b/mailersend/resources/activity.py
@@ -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,8 +45,6 @@ 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._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..5c94687 100644
--- a/mailersend/resources/analytics.py
+++ b/mailersend/resources/analytics.py
@@ -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
@@ -129,3 +121,5 @@ def _build_query_params(
params.pop(f"{field}[]", None)
return params
+
+
diff --git a/mailersend/resources/base.py b/mailersend/resources/base.py
index 8ddd0fa..7977955 100644
--- a/mailersend/resources/base.py
+++ b/mailersend/resources/base.py
@@ -1,8 +1,8 @@
+import inspect
import logging
-from typing import Dict, Any, 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
-import requests
T = TypeVar("T", bound=BaseModel)
@@ -23,9 +23,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 +51,27 @@ def _create_response(
),
)
- def _parse_int_header(
- self, response: requests.Response, header: str
- ) -> Optional[int]:
+ 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.
@@ -110,3 +126,6 @@ def _process_response(
return [cls(**item) for item in response_data]
return response_data
+
+
+ pass
diff --git a/mailersend/resources/dmarc_monitoring.py b/mailersend/resources/dmarc_monitoring.py
index aa63f3a..9382cf4 100644
--- a/mailersend/resources/dmarc_monitoring.py
+++ b/mailersend/resources/dmarc_monitoring.py
@@ -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,8 +199,9 @@ 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)
+
+
diff --git a/mailersend/resources/domains.py b/mailersend/resources/domains.py
index db30010..abeed74 100644
--- a/mailersend/resources/domains.py
+++ b/mailersend/resources/domains.py
@@ -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,8 +181,8 @@ 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)
+
diff --git a/mailersend/resources/email.py b/mailersend/resources/email.py
index b3a1531..4b641e2 100644
--- a/mailersend/resources/email.py
+++ b/mailersend/resources/email.py
@@ -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,6 +72,6 @@ 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._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..1c8e629 100644
--- a/mailersend/resources/email_verification.py
+++ b/mailersend/resources/email_verification.py
@@ -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,11 +195,10 @@ 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)
+
diff --git a/mailersend/resources/identities.py b/mailersend/resources/identities.py
index 78424ae..8598df0 100644
--- a/mailersend/resources/identities.py
+++ b/mailersend/resources/identities.py
@@ -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,8 +192,8 @@ 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)
+
diff --git a/mailersend/resources/inbound.py b/mailersend/resources/inbound.py
index 0c1a5f1..4a6bd6d 100644
--- a/mailersend/resources/inbound.py
+++ b/mailersend/resources/inbound.py
@@ -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,8 +120,6 @@ def delete(self, request: InboundDeleteRequest) -> APIResponse:
)
# Make API request
- response = self.client.request(
- method="DELETE", path=f"inbound/{request.inbound_id}"
- )
+ return self._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..02997f3 100644
--- a/mailersend/resources/messages.py
+++ b/mailersend/resources/messages.py
@@ -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,8 +50,6 @@ 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._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..21e46c0 100644
--- a/mailersend/resources/other.py
+++ b/mailersend/resources/other.py
@@ -20,6 +20,6 @@ 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)
diff --git a/mailersend/resources/recipients.py b/mailersend/resources/recipients.py
index f881d4a..2412c66 100644
--- a/mailersend/resources/recipients.py
+++ b/mailersend/resources/recipients.py
@@ -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,8 +385,8 @@ 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)
+
diff --git a/mailersend/resources/schedules.py b/mailersend/resources/schedules.py
index d3dd405..158a644 100644
--- a/mailersend/resources/schedules.py
+++ b/mailersend/resources/schedules.py
@@ -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,8 +76,8 @@ 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)
+
diff --git a/mailersend/resources/sms_activity.py b/mailersend/resources/sms_activity.py
index a078fc5..6822ae3 100644
--- a/mailersend/resources/sms_activity.py
+++ b/mailersend/resources/sms_activity.py
@@ -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,8 +46,8 @@ 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)
+
diff --git a/mailersend/resources/sms_inbounds.py b/mailersend/resources/sms_inbounds.py
index 9d22afb..760b164 100644
--- a/mailersend/resources/sms_inbounds.py
+++ b/mailersend/resources/sms_inbounds.py
@@ -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,8 +91,8 @@ 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)
+
diff --git a/mailersend/resources/sms_messages.py b/mailersend/resources/sms_messages.py
index 71fdac1..8f33f06 100644
--- a/mailersend/resources/sms_messages.py
+++ b/mailersend/resources/sms_messages.py
@@ -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,8 +40,8 @@ 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)
+
diff --git a/mailersend/resources/sms_numbers.py b/mailersend/resources/sms_numbers.py
index 3bf0410..b4935e0 100644
--- a/mailersend/resources/sms_numbers.py
+++ b/mailersend/resources/sms_numbers.py
@@ -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,8 +83,8 @@ 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)
+
diff --git a/mailersend/resources/sms_recipients.py b/mailersend/resources/sms_recipients.py
index a155675..d08e0d0 100644
--- a/mailersend/resources/sms_recipients.py
+++ b/mailersend/resources/sms_recipients.py
@@ -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,10 +62,10 @@ 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)
+
diff --git a/mailersend/resources/sms_sending.py b/mailersend/resources/sms_sending.py
index 9a1998c..9a71f87 100644
--- a/mailersend/resources/sms_sending.py
+++ b/mailersend/resources/sms_sending.py
@@ -27,6 +27,6 @@ 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)
diff --git a/mailersend/resources/sms_webhooks.py b/mailersend/resources/sms_webhooks.py
index 63dceb0..de9d53c 100644
--- a/mailersend/resources/sms_webhooks.py
+++ b/mailersend/resources/sms_webhooks.py
@@ -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,8 +98,8 @@ 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)
+
diff --git a/mailersend/resources/smtp_users.py b/mailersend/resources/smtp_users.py
index 46727b8..4e307e9 100644
--- a/mailersend/resources/smtp_users.py
+++ b/mailersend/resources/smtp_users.py
@@ -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,10 +118,9 @@ 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)
+
diff --git a/mailersend/resources/templates.py b/mailersend/resources/templates.py
index f26123d..4e8301d 100644
--- a/mailersend/resources/templates.py
+++ b/mailersend/resources/templates.py
@@ -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,9 +78,8 @@ 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)
+
diff --git a/mailersend/resources/tokens.py b/mailersend/resources/tokens.py
index 08d07ff..28e4fce 100644
--- a/mailersend/resources/tokens.py
+++ b/mailersend/resources/tokens.py
@@ -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,9 +112,6 @@ 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}"
- )
+ return self._request(method="DELETE", path=f"token/{request.token_id}")
+
- # Create standardized response
- return self._create_response(response)
diff --git a/mailersend/resources/users.py b/mailersend/resources/users.py
index c58fc31..525fa36 100644
--- a/mailersend/resources/users.py
+++ b/mailersend/resources/users.py
@@ -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,9 +162,6 @@ 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}"
- )
+ return self._request(method="DELETE", path=f"invites/{request.invite_id}", data=lambda r: None)
+
- # Create standardized response
- return self._create_response(response, None)
diff --git a/mailersend/resources/webhooks.py b/mailersend/resources/webhooks.py
index 031c1ff..3454ab9 100644
--- a/mailersend/resources/webhooks.py
+++ b/mailersend/resources/webhooks.py
@@ -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,9 +105,8 @@ 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)
+
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_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": "