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 [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) # Table of Contents + - [Table of Contents](#table-of-contents) - [Installation](#installation) - [Requirements](#requirements) @@ -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": "