diff --git a/README.md b/README.md index 2666f1f..1fa10cd 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,16 @@ MailerSend Python SDK - [Get a single invite](#get-a-single-invite) - [Resend an invite](#resend-an-invite) - [Cancel an invite](#cancel-an-invite) + - [DMARC Monitoring](#dmarc-monitoring) + - [Get a list of monitors](#get-a-list-of-monitors) + - [Create a monitor](#create-a-monitor) + - [Update a monitor](#update-a-monitor) + - [Delete a monitor](#delete-a-monitor) + - [Get aggregated reports](#get-aggregated-reports) + - [Get IP-specific reports](#get-ip-specific-reports) + - [Get report sources](#get-report-sources) + - [Mark IP as favorite](#mark-ip-as-favorite) + - [Remove IP from favorites](#remove-ip-from-favorites) - [Other Endpoints](#other-endpoints) - [Get API Quota](#get-api-quota) - [Error Handling](#error-handling) @@ -2681,6 +2691,143 @@ request = (UsersBuilder() response = ms.users.cancel_invite(request) ``` +## DMARC Monitoring + +### Get a list of monitors + +```python +from mailersend import MailerSendClient, DmarcMonitoringBuilder + +ms = MailerSendClient() + +request = (DmarcMonitoringBuilder() + .page(1) + .limit(25) + .build_list_request()) + +response = ms.dmarc_monitoring.list_monitors(request) +``` + +### Create a monitor + +```python +from mailersend import MailerSendClient, DmarcMonitoringBuilder + +ms = MailerSendClient() + +request = (DmarcMonitoringBuilder() + .domain_id("your-domain-id") + .build_create_request()) + +response = ms.dmarc_monitoring.create_monitor(request) +``` + +### Update a monitor + +```python +from mailersend import MailerSendClient, DmarcMonitoringBuilder + +ms = MailerSendClient() + +request = (DmarcMonitoringBuilder() + .monitor_id("monitor-id") + .wanted_dmarc_record("v=DMARC1; p=reject; rua=mailto:dmarc@example.com") + .build_update_request()) + +response = ms.dmarc_monitoring.update_monitor(request) +``` + +### Delete a monitor + +```python +from mailersend import MailerSendClient, DmarcMonitoringBuilder + +ms = MailerSendClient() + +request = (DmarcMonitoringBuilder() + .monitor_id("monitor-id") + .build_delete_request()) + +response = ms.dmarc_monitoring.delete_monitor(request) +``` + +### Get aggregated reports + +```python +from mailersend import MailerSendClient, DmarcMonitoringBuilder + +ms = MailerSendClient() + +request = (DmarcMonitoringBuilder() + .monitor_id("monitor-id") + .page(1) + .limit(25) + .build_report_request()) + +response = ms.dmarc_monitoring.get_aggregated_report(request) +``` + +### Get IP-specific reports + +```python +from mailersend import MailerSendClient, DmarcMonitoringBuilder + +ms = MailerSendClient() + +request = (DmarcMonitoringBuilder() + .monitor_id("monitor-id") + .ip("192.168.1.1") + .page(1) + .limit(25) + .build_ip_report_request()) + +response = ms.dmarc_monitoring.get_ip_report(request) +``` + +### Get report sources + +```python +from mailersend import MailerSendClient, DmarcMonitoringBuilder + +ms = MailerSendClient() + +request = (DmarcMonitoringBuilder() + .monitor_id("monitor-id") + .build_report_sources_request()) + +response = ms.dmarc_monitoring.get_report_sources(request) +``` + +### Mark IP as favorite + +```python +from mailersend import MailerSendClient, DmarcMonitoringBuilder + +ms = MailerSendClient() + +request = (DmarcMonitoringBuilder() + .monitor_id("monitor-id") + .ip("192.168.1.1") + .build_mark_favorite_request()) + +response = ms.dmarc_monitoring.mark_ip_favorite(request) +``` + +### Remove IP from favorites + +```python +from mailersend import MailerSendClient, DmarcMonitoringBuilder + +ms = MailerSendClient() + +request = (DmarcMonitoringBuilder() + .monitor_id("monitor-id") + .ip("192.168.1.1") + .build_remove_favorite_request()) + +response = ms.dmarc_monitoring.remove_ip_favorite(request) +``` + ## Other Endpoints ### Get API Quota @@ -2799,6 +2946,7 @@ def test_list_sms_recipients(): | 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 dd10077..f8fe436 100644 --- a/mailersend/__init__.py +++ b/mailersend/__init__.py @@ -29,6 +29,7 @@ from .builders.sms_recipients import SmsRecipientsBuilder from .builders.sms_webhooks import SmsWebhooksBuilder from .builders.sms_inbounds import SmsInboundsBuilder +from .builders.dmarc_monitoring import DmarcMonitoringBuilder from .resources.email import Email from .resources.activity import Activity from .resources.analytics import Analytics @@ -39,14 +40,14 @@ EmailPersonalization, EmailRequest, EmailTrackingSettings, - EmailHeader + EmailHeader, ) from .models.activity import ( ActivityRecipient, ActivityEmail, Activity as ActivityModel, ActivityQueryParams, - SingleActivityRequest + SingleActivityRequest, ) from .models.analytics import ( AnalyticsRequest, @@ -58,7 +59,7 @@ ResourceNotFoundError, BadRequestError, ServerError, - ValidationError + ValidationError, ) __version__ = "2.0.0" @@ -66,59 +67,54 @@ __all__ = [ # Core client "MailerSendClient", - # Builders - All available from main module for better UX "EmailBuilder", - "ActivityBuilder", + "ActivityBuilder", "SingleActivityBuilder", "AnalyticsBuilder", "DomainsBuilder", "IdentityBuilder", - "InboundBuilder", + "InboundBuilder", "MessagesBuilder", "SchedulesBuilder", "RecipientsBuilder", "TemplatesBuilder", - "TokensBuilder", + "TokensBuilder", "SmtpUsersBuilder", "WebhooksBuilder", "EmailVerificationBuilder", "UsersBuilder", "SmsMessagesBuilder", - "SmsNumbersBuilder", + "SmsNumbersBuilder", "SmsActivityBuilder", "SmsSendingBuilder", "SmsRecipientsBuilder", "SmsWebhooksBuilder", "SmsInboundsBuilder", - + "DmarcMonitoringBuilder", # Resources "Email", "Activity", "Analytics", "Domains", - # Email models "EmailContact", - "EmailAttachment", + "EmailAttachment", "EmailPersonalization", "EmailRequest", "EmailTrackingSettings", "EmailHeader", - # Activity models "ActivityRecipient", "ActivityEmail", "ActivityModel", "ActivityQueryParams", "SingleActivityRequest", - # Analytics models "AnalyticsRequest", - # Exceptions "MailerSendError", - "AuthenticationError", + "AuthenticationError", "RateLimitExceeded", "ResourceNotFoundError", "BadRequestError", diff --git a/mailersend/builders/__init__.py b/mailersend/builders/__init__.py index 53bb973..75db6d0 100644 --- a/mailersend/builders/__init__.py +++ b/mailersend/builders/__init__.py @@ -26,6 +26,7 @@ from .sms_recipients import SmsRecipientsBuilder from .sms_webhooks import SmsWebhooksBuilder from .sms_inbounds import SmsInboundsBuilder +from .dmarc_monitoring import DmarcMonitoringBuilder __all__ = [ "EmailBuilder", @@ -50,4 +51,5 @@ "SmsRecipientsBuilder", "SmsWebhooksBuilder", "SmsInboundsBuilder", + "DmarcMonitoringBuilder", ] diff --git a/mailersend/builders/dmarc_monitoring.py b/mailersend/builders/dmarc_monitoring.py new file mode 100644 index 0000000..44cf8ec --- /dev/null +++ b/mailersend/builders/dmarc_monitoring.py @@ -0,0 +1,132 @@ +"""Builder for DMARC Monitoring requests.""" + +from typing import Optional + +from ..exceptions import ValidationError +from ..models.dmarc_monitoring import ( + DmarcMonitoringListRequest, + DmarcMonitoringListQueryParams, + DmarcMonitoringCreateRequest, + DmarcMonitoringUpdateRequest, + DmarcMonitoringDeleteRequest, + DmarcMonitoringReportRequest, + DmarcMonitoringReportQueryParams, + DmarcMonitoringIpReportRequest, + DmarcMonitoringReportSourcesRequest, + DmarcMonitoringFavoriteRequest, +) + + +class DmarcMonitoringBuilder: + """Builder for creating DMARC Monitoring requests using a fluent interface.""" + + def __init__(self): + """Initialize a new DmarcMonitoringBuilder.""" + self._reset() + + def _reset(self): + """Reset all builder state.""" + self._monitor_id: Optional[str] = None + self._ip: Optional[str] = None + self._domain_id: Optional[str] = None + self._wanted_dmarc_record: Optional[str] = None + self._page: Optional[int] = None + self._limit: Optional[int] = None + + def monitor_id(self, monitor_id: str) -> "DmarcMonitoringBuilder": + """Set the monitor ID.""" + self._monitor_id = monitor_id + return self + + def ip(self, ip: str) -> "DmarcMonitoringBuilder": + """Set the IP address.""" + self._ip = ip + return self + + def domain_id(self, domain_id: str) -> "DmarcMonitoringBuilder": + """Set the domain ID for creating a monitor.""" + self._domain_id = domain_id + return self + + def wanted_dmarc_record(self, record: str) -> "DmarcMonitoringBuilder": + """Set the wanted DMARC record for updating a monitor.""" + self._wanted_dmarc_record = record + return self + + def page(self, page: int) -> "DmarcMonitoringBuilder": + """Set the page number for pagination.""" + if page < 1: + raise ValidationError("Page must be greater than 0") + self._page = page + return self + + def limit(self, limit: int) -> "DmarcMonitoringBuilder": + """Set the number of items per page.""" + if limit < 10 or limit > 100: + raise ValidationError("Limit must be between 10 and 100") + self._limit = limit + return self + + def build_list_request(self) -> DmarcMonitoringListRequest: + """Build a DmarcMonitoringListRequest.""" + query_params = DmarcMonitoringListQueryParams( + page=self._page if self._page is not None else 1, + limit=self._limit if self._limit is not None else 25, + ) + return DmarcMonitoringListRequest(query_params=query_params) + + def build_create_request(self) -> DmarcMonitoringCreateRequest: + """Build a DmarcMonitoringCreateRequest.""" + return DmarcMonitoringCreateRequest(domain_id=self._domain_id) + + def build_update_request(self) -> DmarcMonitoringUpdateRequest: + """Build a DmarcMonitoringUpdateRequest.""" + return DmarcMonitoringUpdateRequest( + monitor_id=self._monitor_id, + wanted_dmarc_record=self._wanted_dmarc_record, + ) + + def build_delete_request(self) -> DmarcMonitoringDeleteRequest: + """Build a DmarcMonitoringDeleteRequest.""" + return DmarcMonitoringDeleteRequest(monitor_id=self._monitor_id) + + def build_report_request(self) -> DmarcMonitoringReportRequest: + """Build a DmarcMonitoringReportRequest for aggregated reports.""" + query_params = DmarcMonitoringReportQueryParams( + page=self._page if self._page is not None else 1, + limit=self._limit if self._limit is not None else 25, + ) + return DmarcMonitoringReportRequest( + monitor_id=self._monitor_id, + query_params=query_params, + ) + + def build_ip_report_request(self) -> DmarcMonitoringIpReportRequest: + """Build a DmarcMonitoringIpReportRequest for IP-specific reports.""" + query_params = DmarcMonitoringReportQueryParams( + page=self._page if self._page is not None else 1, + limit=self._limit if self._limit is not None else 25, + ) + return DmarcMonitoringIpReportRequest( + monitor_id=self._monitor_id, + ip=self._ip, + query_params=query_params, + ) + + def build_report_sources_request(self) -> DmarcMonitoringReportSourcesRequest: + """Build a DmarcMonitoringReportSourcesRequest.""" + return DmarcMonitoringReportSourcesRequest(monitor_id=self._monitor_id) + + def build_mark_favorite_request(self) -> DmarcMonitoringFavoriteRequest: + """Build a DmarcMonitoringFavoriteRequest for marking an IP as favorite.""" + return DmarcMonitoringFavoriteRequest( + monitor_id=self._monitor_id, + ip=self._ip, + ) + + def build_remove_favorite_request(self) -> DmarcMonitoringFavoriteRequest: + """Build a DmarcMonitoringFavoriteRequest for removing an IP from favorites.""" + return DmarcMonitoringFavoriteRequest( + monitor_id=self._monitor_id, + ip=self._ip, + ) diff --git a/mailersend/client.py b/mailersend/client.py index 99f6935..455c448 100644 --- a/mailersend/client.py +++ b/mailersend/client.py @@ -9,8 +9,12 @@ from .constants import DEFAULT_BASE_URL, DEFAULT_TIMEOUT, USER_AGENT from .exceptions import ( - MailerSendError, AuthenticationError, RateLimitExceeded, - ResourceNotFoundError, BadRequestError, ServerError + MailerSendError, + AuthenticationError, + RateLimitExceeded, + ResourceNotFoundError, + BadRequestError, + ServerError, ) from .resources.email import Email from .resources.activity import Activity @@ -35,28 +39,29 @@ 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 class MailerSendClient: """ Main client for the MailerSend API. - + This client provides access to all MailerSend API resources and handles authentication, request formatting, and error handling. - + Examples: >>> # Using environment variable (recommended) >>> client = MailerSendClient() # Reads from MAILERSEND_API_KEY >>> response = client.emails.send(email_request) - + >>> # Using explicit API key >>> client = MailerSendClient(api_key="your_api_key") - + >>> # Enable debug logging for detailed request/response info >>> client = MailerSendClient(debug=True) """ - + def __init__( self, api_key: Optional[str] = None, @@ -64,60 +69,62 @@ def __init__( timeout: int = DEFAULT_TIMEOUT, max_retries: int = 3, debug: bool = False, - logger: Optional[logging.Logger] = None + logger: Optional[logging.Logger] = None, ) -> None: """ Initialize the MailerSend client. - + Args: - api_key: Your MailerSend API key. If not provided, will try to read + 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 + 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') - + resolved_api_key = api_key or os.getenv("MAILERSEND_API_KEY") + if not resolved_api_key: raise ValueError( "API key is required. Either pass it as 'api_key' parameter or " "set the 'MAILERSEND_API_KEY' environment variable." ) - + self.api_key = resolved_api_key self.base_url = base_url self.timeout = timeout self.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], - allowed_methods=["GET", "POST", "PUT", "DELETE", "PATCH"] + 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}", - "Content-Type": "application/json", - "Accept": "application/json", - "User-Agent": USER_AGENT - }) - + self.session.headers.update( + { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + "Accept": "application/json", + "User-Agent": USER_AGENT, + } + ) + # Initialize resources self.emails = Email(self) self.activities = Activity(self) @@ -142,31 +149,31 @@ def __init__( 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") if debug: self.logger.info("🐛 Debug mode enabled - detailed logging active") - def request( - self, - method: str, - path: str, + self, + method: str, + path: str, params: Optional[Dict[str, Any]] = None, - body: Optional[Dict[str, Any]] = None + body: Optional[Dict[str, Any]] = None, ) -> requests.Response: """ Make an 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 @@ -176,43 +183,43 @@ 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 + 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.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: # 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}) + 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) @@ -220,11 +227,11 @@ def request( 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: @@ -234,28 +241,27 @@ def _get_error_message(self, response: requests.Response) -> str: errors = error_data.get("errors", {}) if errors: error_details = "; ".join( - f"{key}: {', '.join(msgs)}" - for key, msgs in errors.items() + 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") - + def get_debug_info(self) -> Dict[str, Any]: """Get current debug and configuration information.""" return { @@ -264,5 +270,5 @@ def get_debug_info(self) -> Dict[str, Any]: "timeout": self.timeout, "user_agent": USER_AGENT, "logger_level": self.logger.level, - "session_adapters": list(self.session.adapters.keys()) - } \ No newline at end of file + "session_adapters": list(self.session.adapters.keys()), + } diff --git a/mailersend/constants.py b/mailersend/constants.py index f16a6ca..95bd7ea 100644 --- a/mailersend/constants.py +++ b/mailersend/constants.py @@ -8,11 +8,11 @@ # Package info for user agent PACKAGE_NAME = "mailersend-python" -__version__ = "2.0.0" +__version__ = "2.0.3" USER_AGENT = ( f"{PACKAGE_NAME}/{__version__} " f"(Python/{platform.python_version()}; " f"OS/{platform.system()} {platform.release()}; " f"Impl/{platform.python_implementation()})" -) \ No newline at end of file +) diff --git a/mailersend/exceptions.py b/mailersend/exceptions.py index bb9fabf..9611bee 100644 --- a/mailersend/exceptions.py +++ b/mailersend/exceptions.py @@ -4,7 +4,7 @@ class MailerSendError(Exception): """Base exception for all MailerSend API errors.""" - + def __init__(self, message: str, response: Optional[requests.Response] = None): self.message = message self.response = response @@ -13,17 +13,19 @@ def __init__(self, message: str, response: Optional[requests.Response] = None): class AuthenticationError(MailerSendError): """Raised when authentication fails.""" + pass class ResourceNotFoundError(MailerSendError): """Raised when a requested resource is not found.""" + pass class RateLimitExceeded(MailerSendError): """Raised when API rate limits are exceeded.""" - + @property def retry_after(self) -> Optional[int]: """Get the recommended retry time in seconds.""" @@ -37,14 +39,17 @@ def retry_after(self) -> Optional[int]: class BadRequestError(MailerSendError): """Raised when the request was malformed.""" + pass class ServerError(MailerSendError): """Raised when a server-side error occurs.""" + pass class ValidationError(MailerSendError): """Raised when request validation fails.""" - pass \ No newline at end of file + + pass diff --git a/mailersend/models/__init__.py b/mailersend/models/__init__.py index 0a1da39..22237c5 100644 --- a/mailersend/models/__init__.py +++ b/mailersend/models/__init__.py @@ -159,6 +159,16 @@ SmsInboundDeleteRequest, SmsInbound, ) +from .dmarc_monitoring import ( + DmarcMonitoringListRequest, + DmarcMonitoringCreateRequest, + DmarcMonitoringUpdateRequest, + DmarcMonitoringDeleteRequest, + DmarcMonitoringReportRequest, + DmarcMonitoringIpReportRequest, + DmarcMonitoringReportSourcesRequest, + DmarcMonitoringFavoriteRequest, +) __all__ = [ "BaseModel", @@ -289,4 +299,13 @@ "SmsInboundUpdateRequest", "SmsInboundDeleteRequest", "SmsInbound", + # DMARC Monitoring models + "DmarcMonitoringListRequest", + "DmarcMonitoringCreateRequest", + "DmarcMonitoringUpdateRequest", + "DmarcMonitoringDeleteRequest", + "DmarcMonitoringReportRequest", + "DmarcMonitoringIpReportRequest", + "DmarcMonitoringReportSourcesRequest", + "DmarcMonitoringFavoriteRequest", ] diff --git a/mailersend/models/dmarc_monitoring.py b/mailersend/models/dmarc_monitoring.py new file mode 100644 index 0000000..7ed0431 --- /dev/null +++ b/mailersend/models/dmarc_monitoring.py @@ -0,0 +1,180 @@ +"""DMARC Monitoring models.""" + +from typing import Optional, Dict, Any + +from pydantic import Field, field_validator + +from .base import BaseModel + + +class DmarcMonitoringListQueryParams(BaseModel): + """Query parameters for listing DMARC monitors.""" + + page: int = Field(default=1, ge=1) + limit: int = Field(default=25, ge=10, le=100) + + def to_query_params(self) -> Dict[str, Any]: + """Convert to query parameters dictionary, excluding None values.""" + return {"page": self.page, "limit": self.limit} + + +class DmarcMonitoringListRequest(BaseModel): + """Request model for listing DMARC monitors.""" + + query_params: DmarcMonitoringListQueryParams = Field( + default_factory=DmarcMonitoringListQueryParams + ) + + def to_query_params(self) -> Dict[str, Any]: + """Convert to query parameters dictionary.""" + return self.query_params.to_query_params() + + +class DmarcMonitoringCreateRequest(BaseModel): + """Request model for creating a DMARC monitor.""" + + domain_id: str + + @field_validator("domain_id") + @classmethod + def validate_domain_id(cls, v: str) -> str: + """Validate domain_id is provided and not empty.""" + if not v or not v.strip(): + raise ValueError("domain_id cannot be empty") + return v.strip() + + +class DmarcMonitoringUpdateRequest(BaseModel): + """Request model for updating a DMARC monitor.""" + + monitor_id: str + wanted_dmarc_record: str + + @field_validator("monitor_id") + @classmethod + def validate_monitor_id(cls, v: str) -> str: + """Validate monitor_id is provided and not empty.""" + if not v or not v.strip(): + raise ValueError("monitor_id cannot be empty") + return v.strip() + + @field_validator("wanted_dmarc_record") + @classmethod + def validate_wanted_dmarc_record(cls, v: str) -> str: + """Validate wanted_dmarc_record is provided and not empty.""" + if not v or not v.strip(): + raise ValueError("wanted_dmarc_record cannot be empty") + return v.strip() + + +class DmarcMonitoringDeleteRequest(BaseModel): + """Request model for deleting a DMARC monitor.""" + + monitor_id: str + + @field_validator("monitor_id") + @classmethod + def validate_monitor_id(cls, v: str) -> str: + """Validate monitor_id is provided and not empty.""" + if not v or not v.strip(): + raise ValueError("monitor_id cannot be empty") + return v.strip() + + +class DmarcMonitoringReportQueryParams(BaseModel): + """Query parameters for DMARC monitoring reports.""" + + page: int = Field(default=1, ge=1) + limit: int = Field(default=25, ge=10, le=100) + + def to_query_params(self) -> Dict[str, Any]: + """Convert to query parameters dictionary.""" + return {"page": self.page, "limit": self.limit} + + +class DmarcMonitoringReportRequest(BaseModel): + """Request model for getting aggregated DMARC reports.""" + + monitor_id: str + query_params: DmarcMonitoringReportQueryParams = Field( + default_factory=DmarcMonitoringReportQueryParams + ) + + @field_validator("monitor_id") + @classmethod + def validate_monitor_id(cls, v: str) -> str: + """Validate monitor_id is provided and not empty.""" + if not v or not v.strip(): + raise ValueError("monitor_id cannot be empty") + return v.strip() + + def to_query_params(self) -> Dict[str, Any]: + """Convert to query parameters dictionary.""" + return self.query_params.to_query_params() + + +class DmarcMonitoringIpReportRequest(BaseModel): + """Request model for getting IP-specific DMARC reports.""" + + monitor_id: str + ip: str + query_params: DmarcMonitoringReportQueryParams = Field( + default_factory=DmarcMonitoringReportQueryParams + ) + + @field_validator("monitor_id") + @classmethod + def validate_monitor_id(cls, v: str) -> str: + """Validate monitor_id is provided and not empty.""" + if not v or not v.strip(): + raise ValueError("monitor_id cannot be empty") + return v.strip() + + @field_validator("ip") + @classmethod + def validate_ip(cls, v: str) -> str: + """Validate ip is provided and not empty.""" + if not v or not v.strip(): + raise ValueError("ip cannot be empty") + return v.strip() + + def to_query_params(self) -> Dict[str, Any]: + """Convert to query parameters dictionary.""" + return self.query_params.to_query_params() + + +class DmarcMonitoringReportSourcesRequest(BaseModel): + """Request model for getting DMARC report sources.""" + + monitor_id: str + + @field_validator("monitor_id") + @classmethod + def validate_monitor_id(cls, v: str) -> str: + """Validate monitor_id is provided and not empty.""" + if not v or not v.strip(): + raise ValueError("monitor_id cannot be empty") + return v.strip() + + +class DmarcMonitoringFavoriteRequest(BaseModel): + """Request model for marking or removing an IP as favorite.""" + + monitor_id: str + ip: str + + @field_validator("monitor_id") + @classmethod + def validate_monitor_id(cls, v: str) -> str: + """Validate monitor_id is provided and not empty.""" + if not v or not v.strip(): + raise ValueError("monitor_id cannot be empty") + return v.strip() + + @field_validator("ip") + @classmethod + def validate_ip(cls, v: str) -> str: + """Validate ip is provided and not empty.""" + if not v or not v.strip(): + raise ValueError("ip cannot be empty") + return v.strip() diff --git a/mailersend/models/tokens.py b/mailersend/models/tokens.py index 3401ec3..1664c43 100644 --- a/mailersend/models/tokens.py +++ b/mailersend/models/tokens.py @@ -6,7 +6,6 @@ from pydantic import Field, field_validator from .base import BaseModel - # Token scope constants TOKEN_SCOPES = [ "email_full", diff --git a/mailersend/resources/__init__.py b/mailersend/resources/__init__.py index 686428c..44e93ee 100644 --- a/mailersend/resources/__init__.py +++ b/mailersend/resources/__init__.py @@ -25,6 +25,7 @@ from .sms_webhooks import SmsWebhooks from .sms_inbounds import SmsInbounds from .other import Other +from .dmarc_monitoring import DmarcMonitoring __all__ = [ "BaseResource", @@ -50,4 +51,5 @@ "SmsWebhooks", "SmsInbounds", "Other", + "DmarcMonitoring", ] diff --git a/mailersend/resources/dmarc_monitoring.py b/mailersend/resources/dmarc_monitoring.py new file mode 100644 index 0000000..aa63f3a --- /dev/null +++ b/mailersend/resources/dmarc_monitoring.py @@ -0,0 +1,218 @@ +"""DMARC Monitoring resource.""" + +from typing import Optional + +from .base import BaseResource +from ..models.base import APIResponse +from ..models.dmarc_monitoring import ( + DmarcMonitoringListRequest, + DmarcMonitoringListQueryParams, + DmarcMonitoringCreateRequest, + DmarcMonitoringUpdateRequest, + DmarcMonitoringDeleteRequest, + DmarcMonitoringReportRequest, + DmarcMonitoringReportQueryParams, + DmarcMonitoringIpReportRequest, + DmarcMonitoringReportSourcesRequest, + DmarcMonitoringFavoriteRequest, +) + + +class DmarcMonitoring(BaseResource): + """Client for interacting with the MailerSend DMARC Monitoring API.""" + + def list_monitors( + self, request: Optional[DmarcMonitoringListRequest] = None + ) -> APIResponse: + """ + Retrieve a list of DMARC monitors. + + Args: + request: Optional DmarcMonitoringListRequest with pagination options + + Returns: + APIResponse with list of monitors + """ + if request is None: + query_params = DmarcMonitoringListQueryParams() + request = DmarcMonitoringListRequest(query_params=query_params) + + 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) + + def create_monitor(self, request: DmarcMonitoringCreateRequest) -> APIResponse: + """ + Create a new DMARC monitor. + + Args: + request: DmarcMonitoringCreateRequest with domain_id + + Returns: + APIResponse with created monitor information + """ + body = request.model_dump(by_alias=True, exclude_none=True) + 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) + + def update_monitor(self, request: DmarcMonitoringUpdateRequest) -> APIResponse: + """ + Update a DMARC monitor. + + Args: + request: DmarcMonitoringUpdateRequest with monitor_id and wanted_dmarc_record + + Returns: + APIResponse with updated monitor information + """ + body = request.model_dump( + by_alias=True, exclude_none=True, exclude={"monitor_id"} + ) + self.logger.debug( + "Updating DMARC monitor %s with body: %s", request.monitor_id, body + ) + + response = self.client.request( + method="PUT", path=f"dmarc-monitoring/{request.monitor_id}", body=body + ) + return self._create_response(response) + + def delete_monitor(self, request: DmarcMonitoringDeleteRequest) -> APIResponse: + """ + Delete a DMARC monitor. + + Args: + request: DmarcMonitoringDeleteRequest with monitor_id + + Returns: + APIResponse + """ + self.logger.debug("Deleting DMARC monitor: %s", request.monitor_id) + + response = self.client.request( + method="DELETE", path=f"dmarc-monitoring/{request.monitor_id}" + ) + return self._create_response(response) + + def get_aggregated_report( + self, request: DmarcMonitoringReportRequest + ) -> APIResponse: + """ + Get aggregated DMARC reports for a monitor. + + Args: + request: DmarcMonitoringReportRequest with monitor_id and pagination options + + Returns: + APIResponse with aggregated report data + """ + params = request.to_query_params() + self.logger.debug( + "Getting aggregated report for monitor %s with params: %s", + request.monitor_id, + params, + ) + + response = self.client.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: + """ + Get IP-specific DMARC reports for a monitor. + + Args: + request: DmarcMonitoringIpReportRequest with monitor_id, ip, and pagination options + + Returns: + APIResponse with IP-specific report data + """ + params = request.to_query_params() + self.logger.debug( + "Getting IP report for monitor %s, IP %s with params: %s", + request.monitor_id, + request.ip, + params, + ) + + response = self.client.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 + ) -> APIResponse: + """ + Get report sources for a DMARC monitor. + + Args: + request: DmarcMonitoringReportSourcesRequest with monitor_id + + Returns: + APIResponse with report sources data + """ + self.logger.debug("Getting report sources for monitor: %s", request.monitor_id) + + response = self.client.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: + """ + Mark an IP address as favorite for a DMARC monitor. + + Args: + request: DmarcMonitoringFavoriteRequest with monitor_id and ip + + Returns: + APIResponse + """ + self.logger.debug( + "Marking IP %s as favorite for monitor: %s", request.ip, request.monitor_id + ) + + response = self.client.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 + ) -> APIResponse: + """ + Remove an IP address from favorites for a DMARC monitor. + + Args: + request: DmarcMonitoringFavoriteRequest with monitor_id and ip + + Returns: + APIResponse + """ + self.logger.debug( + "Removing IP %s from favorites for monitor: %s", + request.ip, + request.monitor_id, + ) + + response = self.client.request( + method="DELETE", + path=f"dmarc-monitoring/{request.monitor_id}/favorite/{request.ip}", + ) + return self._create_response(response) diff --git a/mailersend/resources/domains.py b/mailersend/resources/domains.py index 142a08c..db30010 100644 --- a/mailersend/resources/domains.py +++ b/mailersend/resources/domains.py @@ -63,7 +63,9 @@ 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}") + response = self.client.request( + method="GET", path=f"domains/{request.domain_id}" + ) return self._create_response(response) @@ -101,7 +103,9 @@ 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}") + response = self.client.request( + method="DELETE", path=f"domains/{request.domain_id}" + ) return self._create_response(response) @@ -193,6 +197,8 @@ def get_domain_verification_status( "Retrieving verification status for domain: %s", request.domain_id ) - response = self.client.request(method="GET", path=f"domains/{request.domain_id}/verify") + response = self.client.request( + method="GET", path=f"domains/{request.domain_id}/verify" + ) return self._create_response(response) diff --git a/mailersend/resources/email.py b/mailersend/resources/email.py index 61a2dde..b3a1531 100644 --- a/mailersend/resources/email.py +++ b/mailersend/resources/email.py @@ -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 - ) + response = self.client.request(method="POST", path="bulk-email", body=payload) return self._create_response(response) @@ -76,8 +74,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}" - ) + response = self.client.request(method="GET", path=f"bulk-email/{bulk_email_id}") return self._create_response(response) diff --git a/mailersend/resources/email_verification.py b/mailersend/resources/email_verification.py index fd84548..d31734c 100644 --- a/mailersend/resources/email_verification.py +++ b/mailersend/resources/email_verification.py @@ -35,7 +35,9 @@ def verify_email(self, request: EmailVerifyRequest) -> APIResponse: self.logger.debug("Verifying email address: %s", body) # Make API call - response = self.client.request(method="POST", path="email-verification/verify", body=body) + response = self.client.request( + method="POST", path="email-verification/verify", body=body + ) # Create standardized response return self._create_response(response) @@ -164,7 +166,9 @@ def create_verification( ) # Make API call - response = self.client.request(method="POST", path="email-verification", body=body) + response = self.client.request( + method="POST", path="email-verification", body=body + ) # Create standardized response return self._create_response(response) diff --git a/mailersend/resources/other.py b/mailersend/resources/other.py index 354ab0f..27fa586 100644 --- a/mailersend/resources/other.py +++ b/mailersend/resources/other.py @@ -7,19 +7,19 @@ class Other(BaseResource): """ Client for interacting with other MailerSend API endpoints. - + Provides methods for accessing miscellaneous endpoints like API quota. """ - + def get_quota(self) -> APIResponse: """ Get API quota information. - + Returns: APIResponse with quota information including remaining requests """ self.logger.debug("Retrieving API quota information") - + response = self.client.request(method="GET", path="api-quota") - - return self._create_response(response) \ No newline at end of file + + return self._create_response(response) diff --git a/mailersend/resources/recipients.py b/mailersend/resources/recipients.py index 33d4f7e..f881d4a 100644 --- a/mailersend/resources/recipients.py +++ b/mailersend/resources/recipients.py @@ -43,9 +43,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 - ) + response = self.client.request(method="GET", path="recipients", params=params) return self._create_response(response) diff --git a/mailersend/resources/sms_activity.py b/mailersend/resources/sms_activity.py index e4e105f..a078fc5 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 - ) + response = self.client.request(method="GET", path="sms-activity", params=params) return self._create_response(response) diff --git a/mailersend/resources/sms_inbounds.py b/mailersend/resources/sms_inbounds.py index 590e16c..9d22afb 100644 --- a/mailersend/resources/sms_inbounds.py +++ b/mailersend/resources/sms_inbounds.py @@ -27,9 +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 - ) + response = self.client.request(method="GET", path="sms-inbounds", params=params) return self._create_response(response) def get_sms_inbound(self, request: SmsInboundGetRequest) -> APIResponse: diff --git a/mailersend/resources/sms_messages.py b/mailersend/resources/sms_messages.py index 59d99f6..71fdac1 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 - ) + response = self.client.request(method="GET", path="sms-messages", params=params) return self._create_response(response) diff --git a/mailersend/resources/sms_numbers.py b/mailersend/resources/sms_numbers.py index 9c4ce59..3bf0410 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 - ) + response = self.client.request(method="GET", path="sms-numbers", params=params) return self._create_response(response) diff --git a/mailersend/resources/sms_webhooks.py b/mailersend/resources/sms_webhooks.py index b79a3d4..63dceb0 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 - ) + response = self.client.request(method="GET", path="sms-webhooks", params=params) return self._create_response(response) diff --git a/mailersend/resources/templates.py b/mailersend/resources/templates.py index d124955..f26123d 100644 --- a/mailersend/resources/templates.py +++ b/mailersend/resources/templates.py @@ -45,9 +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 - ) + response = self.client.request(method="GET", path="templates", params=params) # Create standardized response return self._create_response(response) diff --git a/mailersend/resources/tokens.py b/mailersend/resources/tokens.py index 09df098..08d07ff 100644 --- a/mailersend/resources/tokens.py +++ b/mailersend/resources/tokens.py @@ -49,9 +49,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}" - ) + response = self.client.request(method="GET", 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 5e57f78..c58fc31 100644 --- a/mailersend/resources/users.py +++ b/mailersend/resources/users.py @@ -54,9 +54,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}" - ) + response = self.client.request(method="GET", path=f"users/{request.user_id}") # Create standardized response return self._create_response(response) @@ -115,9 +113,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}" - ) + response = self.client.request(method="DELETE", path=f"users/{request.user_id}") # Create standardized response return self._create_response(response, None) diff --git a/mailersend/utils/__init__.py b/mailersend/utils/__init__.py index 6803051..b98559f 100644 --- a/mailersend/utils/__init__.py +++ b/mailersend/utils/__init__.py @@ -8,4 +8,4 @@ __all__ = [ "process_file_attachments", "validate_email_requirements", -] \ No newline at end of file +] diff --git a/mailersend/utils/files.py b/mailersend/utils/files.py index bb42a10..ed8c3bd 100644 --- a/mailersend/utils/files.py +++ b/mailersend/utils/files.py @@ -8,46 +8,51 @@ logger = logging.getLogger(__name__) -def process_file_attachments(attachments: List[Dict[str, Any]]) -> List[EmailAttachment]: + +def process_file_attachments( + attachments: List[Dict[str, Any]], +) -> List[EmailAttachment]: """ Process file attachments by reading file content and encoding as base64. - + Args: attachments: List of attachment dictionaries with possible 'file_path' keys - + Returns: List of processed Attachment objects - + Raises: ValidationError: If file cannot be read or attachment data is invalid """ processed_attachments = [] - + for attachment_data in attachments: # Handle file_path if present - if isinstance(attachment_data, dict) and 'file_path' in attachment_data: - file_path = attachment_data.pop('file_path') - + if isinstance(attachment_data, dict) and "file_path" in attachment_data: + file_path = attachment_data.pop("file_path") + try: # Read and encode file content - with open(file_path, 'rb') as file: - file_content = base64.b64encode(file.read()).decode('utf-8') - + with open(file_path, "rb") as file: + file_content = base64.b64encode(file.read()).decode("utf-8") + # Use filename from path if not provided - if 'filename' not in attachment_data: - attachment_data['filename'] = os.path.basename(file_path) - + if "filename" not in attachment_data: + attachment_data["filename"] = os.path.basename(file_path) + # Set default disposition if not provided - if 'disposition' not in attachment_data: - attachment_data['disposition'] = 'attachment' - + if "disposition" not in attachment_data: + attachment_data["disposition"] = "attachment" + # Add file content - attachment_data['content'] = file_content - + attachment_data["content"] = file_content + except IOError as e: logger.error(f"Failed to read attachment file {file_path}: {str(e)}") - raise ValidationError(f"Cannot read attachment file {file_path}: {str(e)}") - + raise ValidationError( + f"Cannot read attachment file {file_path}: {str(e)}" + ) + # Create Attachment object from the dict try: attachment = EmailAttachment(**attachment_data) @@ -56,5 +61,5 @@ def process_file_attachments(attachments: List[Dict[str, Any]]) -> List[EmailAtt except Exception as e: logger.error(f"Failed to create Attachment object: {str(e)}") raise ValidationError(f"Invalid attachment data: {str(e)}") - - return processed_attachments \ No newline at end of file + + return processed_attachments diff --git a/mailersend/utils/validators.py b/mailersend/utils/validators.py index e4776bf..f1302e8 100644 --- a/mailersend/utils/validators.py +++ b/mailersend/utils/validators.py @@ -1,28 +1,29 @@ from ..exceptions import ValidationError from ..models.email import EmailRequest + def validate_email_requirements(email: EmailRequest) -> None: """ Validate email request based on conditional requirements. - + Args: email: EmailRequest object to validate - + Raises: ValidationError: If validation fails """ # Template validation has_template = email.template_id is not None has_content = email.text is not None or email.html is not None - + # Check if we have content or template if not has_template and not has_content: raise ValidationError("Either template_id or text/html content is required") - + # Check subject is provided if no template with default subject if not email.subject and not has_template: raise ValidationError("Subject is required when not using a template") - + # Check from email is provided if no template with default sender if not email.from_email and not has_template: - raise ValidationError("From email is required when not using a template") \ No newline at end of file + raise ValidationError("From email is required when not using a template") diff --git a/poetry.lock b/poetry.lock index 0138371..b4be278 100644 --- a/poetry.lock +++ b/poetry.lock @@ -371,118 +371,118 @@ files = [ [[package]] name = "coverage" -version = "7.13.4" +version = "7.13.5" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415"}, - {file = "coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b"}, - {file = "coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a"}, - {file = "coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f"}, - {file = "coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012"}, - {file = "coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def"}, - {file = "coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256"}, - {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda"}, - {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92"}, - {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c"}, - {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58"}, - {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9"}, - {file = "coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf"}, - {file = "coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95"}, - {file = "coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053"}, - {file = "coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11"}, - {file = "coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa"}, - {file = "coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7"}, - {file = "coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00"}, - {file = "coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef"}, - {file = "coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903"}, - {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f"}, - {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299"}, - {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505"}, - {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6"}, - {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9"}, - {file = "coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9"}, - {file = "coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f"}, - {file = "coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f"}, - {file = "coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459"}, - {file = "coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3"}, - {file = "coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634"}, - {file = "coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3"}, - {file = "coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa"}, - {file = "coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3"}, - {file = "coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a"}, - {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7"}, - {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc"}, - {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47"}, - {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985"}, - {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0"}, - {file = "coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246"}, - {file = "coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126"}, - {file = "coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d"}, - {file = "coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9"}, - {file = "coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac"}, - {file = "coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea"}, - {file = "coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b"}, - {file = "coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525"}, - {file = "coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242"}, - {file = "coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148"}, - {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a"}, - {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23"}, - {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80"}, - {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea"}, - {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a"}, - {file = "coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d"}, - {file = "coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd"}, - {file = "coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af"}, - {file = "coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d"}, - {file = "coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12"}, - {file = "coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b"}, - {file = "coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9"}, - {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092"}, - {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9"}, - {file = "coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26"}, - {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2"}, - {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940"}, - {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c"}, - {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0"}, - {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b"}, - {file = "coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9"}, - {file = "coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd"}, - {file = "coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997"}, - {file = "coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601"}, - {file = "coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689"}, - {file = "coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c"}, - {file = "coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129"}, - {file = "coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552"}, - {file = "coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a"}, - {file = "coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356"}, - {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71"}, - {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5"}, - {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98"}, - {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5"}, - {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0"}, - {file = "coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb"}, - {file = "coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505"}, - {file = "coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2"}, - {file = "coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056"}, - {file = "coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc"}, - {file = "coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9"}, - {file = "coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf"}, - {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55"}, - {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72"}, - {file = "coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a"}, - {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6"}, - {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3"}, - {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750"}, - {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39"}, - {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0"}, - {file = "coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea"}, - {file = "coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932"}, - {file = "coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b"}, - {file = "coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0"}, - {file = "coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91"}, + {file = "coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5"}, + {file = "coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0"}, + {file = "coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58"}, + {file = "coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e"}, + {file = "coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d"}, + {file = "coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8"}, + {file = "coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf"}, + {file = "coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9"}, + {file = "coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028"}, + {file = "coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01"}, + {file = "coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c"}, + {file = "coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf"}, + {file = "coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810"}, + {file = "coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de"}, + {file = "coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1"}, + {file = "coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17"}, + {file = "coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85"}, + {file = "coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b"}, + {file = "coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664"}, + {file = "coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d"}, + {file = "coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2"}, + {file = "coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a"}, + {file = "coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819"}, + {file = "coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911"}, + {file = "coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f"}, + {file = "coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0"}, + {file = "coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc"}, + {file = "coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633"}, + {file = "coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8"}, + {file = "coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b"}, + {file = "coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a"}, + {file = "coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215"}, + {file = "coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43"}, + {file = "coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45"}, + {file = "coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61"}, + {file = "coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179"}, ] [package.extras] @@ -1531,25 +1531,25 @@ md = ["cmarkgfm (>=0.8.0)"] [[package]] name = "requests" -version = "2.32.5" +version = "2.33.1" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main", "dev"] files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, + {file = "requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a"}, + {file = "requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517"}, ] [package.dependencies] -certifi = ">=2017.4.17" +certifi = ">=2023.5.7" charset_normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" +urllib3 = ">=1.26,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] [[package]] name = "requests-toolbelt" diff --git a/pyproject.toml b/pyproject.toml index e013d10..5b6dc5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ authors = ["MailerLite "] description = "The official MailerLite Python SDK" name = "mailersend" -version = "2.0.0" +version = "2.0.3" [tool.poetry.dependencies] python = "^3.10" diff --git a/tests/conftest.py b/tests/conftest.py index cf5e09b..70cbe7e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,53 +17,47 @@ def sanitize_response_body(response): try: # Get the response body - handle different VCR formats body = None - if response.get('body'): - if isinstance(response['body'], dict): - body = response['body'].get('string') + if response.get("body"): + if isinstance(response["body"], dict): + body = response["body"].get("string") else: - body = response['body'] - + body = response["body"] + if not body: return response - + # Convert bytes to string if needed if isinstance(body, bytes): - body = body.decode('utf-8') - + body = body.decode("utf-8") + # Only process if it looks like JSON (contains accessToken) - if 'accessToken' in body or 'mlsn.' in body: + if "accessToken" in body or "mlsn." in body: # Replace accessToken values body = re.sub( r'"accessToken":"mlsn\.[a-f0-9]+"', '"accessToken":"***FILTERED***"', - body + body, ) - + # Replace any other mlsn tokens - body = re.sub( - r'"mlsn\.[a-f0-9]{60,}"', - '"***FILTERED***"', - body - ) - + body = re.sub(r'"mlsn\.[a-f0-9]{60,}"', '"***FILTERED***"', body) + # Replace preview tokens body = re.sub( - r'"preview":"mlsn\.[a-f0-9]+"', - '"preview":"***FILTERED***"', - body + r'"preview":"mlsn\.[a-f0-9]+"', '"preview":"***FILTERED***"', body ) - + # Update the response body (convert back to bytes for VCR) - if isinstance(response['body'], dict): - response['body']['string'] = body.encode('utf-8') + if isinstance(response["body"], dict): + response["body"]["string"] = body.encode("utf-8") else: - response['body'] = body.encode('utf-8') - + response["body"] = body.encode("utf-8") + except Exception as e: print(f"[VCR FILTER] Error sanitizing response: {e}") # Don't fail tests if filtering fails pass - + return response @@ -78,6 +72,7 @@ def sanitize_response_body(response): before_record_response=sanitize_response_body, ) + # Create a pytest fixture for the API key @pytest.fixture def api_key(): diff --git a/tests/integration/test_activity.py b/tests/integration/test_activity.py index 6b0b061..4381fac 100644 --- a/tests/integration/test_activity.py +++ b/tests/integration/test_activity.py @@ -21,7 +21,7 @@ def base_activity_request(test_domain_id): # Use fixed timestamps for VCR consistency # These are the exact timestamps recorded in the VCR cassettes date_from = 1754040747 # Fixed timestamp from cassette - date_to = 1754044347 # Fixed timestamp from cassette (1 hour later) + date_to = 1754044347 # Fixed timestamp from cassette (1 hour later) query_params = ActivityQueryParams( date_from=date_from, date_to=date_to, page=1, limit=25 @@ -372,8 +372,8 @@ def test_get_single_activity_validation_error(self): with pytest.raises(ValueError) as exc_info: SingleActivityBuilder().activity_id("").build() assert "activity_id is required" in str(exc_info.value) - - # Test whitespace activity_id at model level + + # Test whitespace activity_id at model level with pytest.raises(ValueError) as exc_info: SingleActivityBuilder().activity_id(" ").build() assert "activity_id cannot be empty" in str(exc_info.value) diff --git a/tests/integration/test_analytics.py b/tests/integration/test_analytics.py index 91a3ce4..cb207f5 100644 --- a/tests/integration/test_analytics.py +++ b/tests/integration/test_analytics.py @@ -299,15 +299,13 @@ def test_date_builder_helpers(self): def test_activity_by_date_error_no_events(self): """Test that activity by date requires events""" from mailersend.exceptions import BadRequestError - + request = self.analytics_request_factory( self.base_analytics_request, event=None, # No events specified - should cause error ) - with pytest.raises( - BadRequestError, match="The event must be an array" - ): + with pytest.raises(BadRequestError, match="The event must be an array"): self.email_client.analytics.get_activity_by_date(request) @vcr.use_cassette("tests/fixtures/cassettes/analytics_comprehensive_test.yaml") @@ -352,4 +350,4 @@ def test_comprehensive_analytics_workflow(self): # All should return valid data structures for response in [date_response, country_response, ua_response, env_response]: assert "data" in response.data - assert "stats" in response.data["data"] \ No newline at end of file + assert "stats" in response.data["data"] diff --git a/tests/integration/test_email_send.py b/tests/integration/test_email_send.py index 35495e9..f2c44c3 100644 --- a/tests/integration/test_email_send.py +++ b/tests/integration/test_email_send.py @@ -103,7 +103,7 @@ def test_send_with_text_priority(self): email_request = self.email_request_factory( self.base_email_request, text="This is a plain text email for testing.", - precedence_bulk=True + precedence_bulk=True, ) result = self.email_client.emails.send(email_request) @@ -302,7 +302,7 @@ def test_bulk_email_workflow(self): assert "bulk_email_id" in send_result assert send_result["bulk_email_id"] is not None assert send_result.status_code == 202 - + # Step 2: Get bulk status using the ID from the send operation bulk_email_id = send_result["bulk_email_id"] status_result = self.email_client.emails.get_bulk_status(bulk_email_id) diff --git a/tests/integration/test_email_verification.py b/tests/integration/test_email_verification.py index fc6be3b..e9f829a 100644 --- a/tests/integration/test_email_verification.py +++ b/tests/integration/test_email_verification.py @@ -48,7 +48,9 @@ def test_list_email_verification_lists_basic( self, email_client, basic_list_request ): """Test listing email verification lists with basic parameters.""" - response = email_client.email_verification.list_verifications(basic_list_request) + response = email_client.email_verification.list_verifications( + basic_list_request + ) assert isinstance(response, APIResponse) assert response.status_code == 200 @@ -116,11 +118,13 @@ def test_get_email_verification_list_not_found_with_test_id( ): """Test getting a non-existent email verification list returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.email_verification.get_verification(verification_get_request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) + assert "not found" in str(exc_info.value).lower() or "404" in str( + exc_info.value + ) @vcr.use_cassette("email_verification_get_not_found.yaml") def test_get_email_verification_list_not_found(self, email_client): @@ -143,7 +147,7 @@ def test_verify_email_verification_list_not_found( ): """Test verifying a non-existent email verification list returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + verify_request = EmailVerificationVerifyRequest( email_verification_id=verification_get_request.email_verification_id ) @@ -151,7 +155,9 @@ def test_verify_email_verification_list_not_found( with pytest.raises(ResourceNotFoundError) as exc_info: email_client.email_verification.verify_list(verify_request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) + assert "not found" in str(exc_info.value).lower() or "404" in str( + exc_info.value + ) @vcr.use_cassette("email_verification_get_results.yaml") def test_get_email_verification_results_not_found( @@ -159,16 +165,18 @@ def test_get_email_verification_results_not_found( ): """Test getting results for a non-existent email verification list returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + results_request = EmailVerificationResultsRequest( email_verification_id=verification_get_request.email_verification_id, - query_params=EmailVerificationResultsQueryParams(page=1, limit=10) + query_params=EmailVerificationResultsQueryParams(page=1, limit=10), ) with pytest.raises(ResourceNotFoundError) as exc_info: email_client.email_verification.get_results(results_request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) + assert "not found" in str(exc_info.value).lower() or "404" in str( + exc_info.value + ) @vcr.use_cassette("email_verification_comprehensive_workflow.yaml") def test_comprehensive_email_verification_workflow( @@ -181,7 +189,9 @@ def test_comprehensive_email_verification_workflow( name="Comprehensive Test List", emails=sample_email_list ) - create_response = email_client.email_verification.create_verification(create_request) + create_response = email_client.email_verification.create_verification( + create_request + ) assert isinstance(create_response, APIResponse) assert create_response.status_code in [200, 201] @@ -212,12 +222,10 @@ def test_comprehensive_email_verification_workflow( # Step 4: Get results (may not be ready immediately in real scenario) results_request = EmailVerificationResultsRequest( email_verification_id=created_list_id, - query_params=EmailVerificationResultsQueryParams(page=1, limit=10) + query_params=EmailVerificationResultsQueryParams(page=1, limit=10), ) - results_response = email_client.email_verification.get_results( - results_request - ) + results_response = email_client.email_verification.get_results(results_request) assert isinstance(results_response, APIResponse) assert results_response.status_code == 200 @@ -240,7 +248,9 @@ def test_create_email_verification_list_validation_error(self, email_client): @vcr.use_cassette("email_verification_api_response_structure.yaml") def test_api_response_structure(self, email_client, basic_list_request): """Test that API response has the expected structure and metadata.""" - response = email_client.email_verification.list_verifications(basic_list_request) + response = email_client.email_verification.list_verifications( + basic_list_request + ) assert isinstance(response, APIResponse) assert response.status_code == 200 @@ -266,17 +276,16 @@ def test_list_email_verification_validation_error(self, email_client): # Should raise an AttributeError for invalid request type error_str = str(exc_info.value).lower() - assert ( - "attribute" in error_str - or "to_query_params" in error_str - ) + assert "attribute" in error_str or "to_query_params" in error_str @vcr.use_cassette("email_verification_empty_list.yaml") def test_list_email_verification_empty_result( self, email_client, basic_list_request ): """Test listing email verification lists when no lists exist.""" - response = email_client.email_verification.list_verifications(basic_list_request) + response = email_client.email_verification.list_verifications( + basic_list_request + ) assert isinstance(response, APIResponse) assert response.status_code == 200 diff --git a/tests/integration/test_identities.py b/tests/integration/test_identities.py index 3797131..54df4f7 100644 --- a/tests/integration/test_identities.py +++ b/tests/integration/test_identities.py @@ -19,9 +19,7 @@ @pytest.fixture def basic_identity_list_request(): """Basic identity list request""" - return IdentityListRequest( - query_params=IdentityListQueryParams(page=1, limit=10) - ) + return IdentityListRequest(query_params=IdentityListQueryParams(page=1, limit=10)) @pytest.fixture @@ -40,15 +38,17 @@ def sample_identity_data(): "reply_to_email": os.environ.get("SDK_FROM_EMAIL", "reply@example.com"), "reply_to_name": "Reply Test", "add_note": True, - "personal_note": "Test identity for integration testing" + "personal_note": "Test identity for integration testing", } class TestIdentitiesIntegration: @vcr.use_cassette("identities_create_not_available.yaml") - def test_create_identity_endpoint_not_available(self, email_client, sample_identity_data): + def test_create_identity_endpoint_not_available( + self, email_client, sample_identity_data + ): from mailersend.exceptions import BadRequestError - + request = IdentityCreateRequest( domain_id=sample_identity_data["domain_id"], name=sample_identity_data["name"], @@ -56,7 +56,7 @@ def test_create_identity_endpoint_not_available(self, email_client, sample_ident reply_to_email=sample_identity_data["reply_to_email"], reply_to_name=sample_identity_data["reply_to_name"], add_note=sample_identity_data["add_note"], - personal_note=sample_identity_data["personal_note"] + personal_note=sample_identity_data["personal_note"], ) with pytest.raises(BadRequestError) as exc_info: @@ -66,9 +66,11 @@ def test_create_identity_endpoint_not_available(self, email_client, sample_ident assert "the email has already been taken." in error_str @vcr.use_cassette("identities_get_single.yaml") - def test_get_identity_endpoint_not_available(self, email_client, identity_get_request): + def test_get_identity_endpoint_not_available( + self, email_client, identity_get_request + ): from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.identities.get_identity(identity_get_request) @@ -79,7 +81,7 @@ def test_get_identity_endpoint_not_available(self, email_client, identity_get_re def test_get_identity_by_email_endpoint_not_available(self, email_client): """Test that get identity by email endpoint is not available.""" from mailersend.exceptions import ResourceNotFoundError - + request = IdentityGetByEmailRequest(email="test@example.com") with pytest.raises(ResourceNotFoundError) as exc_info: @@ -92,10 +94,9 @@ def test_get_identity_by_email_endpoint_not_available(self, email_client): def test_update_identity_endpoint_not_available(self, email_client): """Test that update identity endpoint is not available.""" from mailersend.exceptions import ResourceNotFoundError - + request = IdentityUpdateRequest( - identity_id="test-identity-id", - name="Updated Test Identity" + identity_id="test-identity-id", name="Updated Test Identity" ) with pytest.raises(ResourceNotFoundError) as exc_info: @@ -108,10 +109,9 @@ def test_update_identity_endpoint_not_available(self, email_client): def test_update_identity_by_email_endpoint_not_available(self, email_client): """Test that update identity by email endpoint is not available.""" from mailersend.exceptions import ResourceNotFoundError - + request = IdentityUpdateByEmailRequest( - email="test@example.com", - name="Updated Test Identity" + email="test@example.com", name="Updated Test Identity" ) with pytest.raises(ResourceNotFoundError) as exc_info: @@ -121,10 +121,12 @@ def test_update_identity_by_email_endpoint_not_available(self, email_client): assert "could not be found" in error_str or "not found" in error_str @vcr.use_cassette("identities_delete_by_id.yaml") - def test_delete_identity_endpoint_not_available(self, email_client, identity_get_request): + def test_delete_identity_endpoint_not_available( + self, email_client, identity_get_request + ): """Test that delete identity endpoint is not available.""" from mailersend.exceptions import ResourceNotFoundError - + delete_request = IdentityDeleteRequest( identity_id=identity_get_request.identity_id ) @@ -139,7 +141,7 @@ def test_delete_identity_endpoint_not_available(self, email_client, identity_get def test_delete_identity_by_email_endpoint_not_available(self, email_client): """Test that delete identity by email endpoint is not available.""" from mailersend.exceptions import ResourceNotFoundError - + request = IdentityDeleteByEmailRequest(email="test@example.com") with pytest.raises(ResourceNotFoundError) as exc_info: @@ -157,46 +159,31 @@ def test_list_identities_validation_error(self, email_client): # Should raise an AttributeError for invalid request type error_str = str(exc_info.value).lower() - assert ( - "attribute" in error_str - or "to_query_params" in error_str - ) + assert "attribute" in error_str or "to_query_params" in error_str def test_create_identity_model_validation(self): """Test model validation for identity creation.""" # Test empty domain_id with pytest.raises(ValueError) as exc_info: - IdentityCreateRequest( - domain_id="", - name="Test", - email="test@example.com" - ) + IdentityCreateRequest(domain_id="", name="Test", email="test@example.com") assert "domain id is required" in str(exc_info.value).lower() # Test empty name with pytest.raises(ValueError) as exc_info: IdentityCreateRequest( - domain_id="test-domain", - name="", - email="test@example.com" + domain_id="test-domain", name="", email="test@example.com" ) assert "name is required" in str(exc_info.value).lower() # Test empty email with pytest.raises(ValueError) as exc_info: - IdentityCreateRequest( - domain_id="test-domain", - name="Test", - email="" - ) + IdentityCreateRequest(domain_id="test-domain", name="Test", email="") assert "email is required" in str(exc_info.value).lower() # Test invalid email format with pytest.raises(ValueError) as exc_info: IdentityCreateRequest( - domain_id="test-domain", - name="Test", - email="invalid-email" + domain_id="test-domain", name="Test", email="invalid-email" ) assert "invalid email format" in str(exc_info.value).lower() @@ -207,15 +194,15 @@ def test_identity_list_query_params_validation(self): assert params.page == 1 assert params.limit == 25 assert params.domain_id == "test-domain" - + # Test minimum limit validation with pytest.raises(ValueError): IdentityListQueryParams(limit=5) # Below minimum of 10 - - # Test maximum limit validation + + # Test maximum limit validation with pytest.raises(ValueError): IdentityListQueryParams(limit=150) # Above maximum of 100 - + # Test minimum page validation with pytest.raises(ValueError): IdentityListQueryParams(page=0) # Below minimum of 1 @@ -226,13 +213,13 @@ def test_identity_update_request_validation(self): with pytest.raises(ValueError) as exc_info: IdentityUpdateRequest(identity_id="", name="Test") assert "identity id is required" in str(exc_info.value).lower() - + # Test empty email for email-based updates with pytest.raises(ValueError) as exc_info: IdentityUpdateByEmailRequest(email="", name="Test") assert "email is required" in str(exc_info.value).lower() - + # Test invalid email format for email-based updates with pytest.raises(ValueError) as exc_info: IdentityUpdateByEmailRequest(email="invalid-email", name="Test") - assert "invalid email format" in str(exc_info.value).lower() \ No newline at end of file + assert "invalid email format" in str(exc_info.value).lower() diff --git a/tests/integration/test_inbound.py b/tests/integration/test_inbound.py index 3de5b66..dcf4fe9 100644 --- a/tests/integration/test_inbound.py +++ b/tests/integration/test_inbound.py @@ -19,9 +19,7 @@ @pytest.fixture def basic_inbound_list_request(): """Basic inbound list request""" - return InboundListRequest( - query_params=InboundListQueryParams(page=1, limit=10) - ) + return InboundListRequest(query_params=InboundListQueryParams(page=1, limit=10)) @pytest.fixture @@ -50,8 +48,10 @@ def sample_inbound_data(test_domain_id): "match_filter": InboundFilterGroup(type="match_all"), "match_type": "all", "forwards": [ - InboundForward(type="email", value=os.environ.get("SDK_FROM_EMAIL", "test@example.com")) - ] + InboundForward( + type="email", value=os.environ.get("SDK_FROM_EMAIL", "test@example.com") + ) + ], } @@ -66,28 +66,24 @@ def sample_inbound_data_with_domain(test_domain_id): "inbound_priority": 50, "catch_filter": InboundFilterGroup( type="catch_recipient", - filters=[ - { - "comparer": "equal", - "value": "support@example.com" - } - ] + filters=[{"comparer": "equal", "value": "support@example.com"}], ), "catch_type": "all", "match_filter": InboundFilterGroup( type="match_sender", - filters=[ - { - "comparer": "contains", - "value": "@trusted.com" - } - ] + filters=[{"comparer": "contains", "value": "@trusted.com"}], ), "match_type": "all", "forwards": [ - InboundForward(type="email", value=os.environ.get("SDK_FROM_EMAIL", "test@example.com")), - InboundForward(type="webhook", value="https://example.com/webhook", secret="webhook-secret") - ] + InboundForward( + type="email", value=os.environ.get("SDK_FROM_EMAIL", "test@example.com") + ), + InboundForward( + type="webhook", + value="https://example.com/webhook", + secret="webhook-secret", + ), + ], } @@ -146,9 +142,7 @@ def test_list_inbound_routes_with_domain_filter(self, email_client, test_domain_ """Test listing inbound routes filtered by domain.""" request = InboundListRequest( query_params=InboundListQueryParams( - page=1, - limit=10, - domain_id=test_domain_id + page=1, limit=10, domain_id=test_domain_id ) ) @@ -162,17 +156,23 @@ def test_list_inbound_routes_with_domain_filter(self, email_client, test_domain_ def test_get_inbound_route_not_found(self, email_client, inbound_get_request): """Test getting a non-existent inbound route returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.inbound.get(inbound_get_request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) or "could not be found" in str(exc_info.value).lower() + assert ( + "not found" in str(exc_info.value).lower() + or "404" in str(exc_info.value) + or "could not be found" in str(exc_info.value).lower() + ) @vcr.use_cassette("inbound_create_invalid_domain.yaml") - def test_create_inbound_route_invalid_domain(self, email_client, sample_inbound_data): + def test_create_inbound_route_invalid_domain( + self, email_client, sample_inbound_data + ): """Test creating inbound route with invalid domain ID returns 422.""" from mailersend.exceptions import BadRequestError - + request = InboundCreateRequest( domain_id="invalid-domain-id", **{k: v for k, v in sample_inbound_data.items() if k != "domain_id"} @@ -182,13 +182,17 @@ def test_create_inbound_route_invalid_domain(self, email_client, sample_inbound_ email_client.inbound.create(request) error_str = str(exc_info.value).lower() - assert "domain" in error_str and ("invalid" in error_str or "required" in error_str or "not found" in error_str) + assert "domain" in error_str and ( + "invalid" in error_str + or "required" in error_str + or "not found" in error_str + ) @vcr.use_cassette("inbound_update_not_found.yaml") def test_update_inbound_route_not_found(self, email_client, sample_inbound_data): """Test updating non-existent inbound route returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + request = InboundUpdateRequest( inbound_id="test-inbound-id", **{k: v for k, v in sample_inbound_data.items() if k != "domain_id"} @@ -197,21 +201,27 @@ def test_update_inbound_route_not_found(self, email_client, sample_inbound_data) with pytest.raises(ResourceNotFoundError) as exc_info: email_client.inbound.update(request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) or "could not be found" in str(exc_info.value).lower() + assert ( + "not found" in str(exc_info.value).lower() + or "404" in str(exc_info.value) + or "could not be found" in str(exc_info.value).lower() + ) @vcr.use_cassette("inbound_delete_not_found.yaml") def test_delete_inbound_route_not_found(self, email_client, inbound_get_request): """Test deleting non-existent inbound route returns 404.""" from mailersend.exceptions import ResourceNotFoundError - - delete_request = InboundDeleteRequest( - inbound_id=inbound_get_request.inbound_id - ) + + delete_request = InboundDeleteRequest(inbound_id=inbound_get_request.inbound_id) with pytest.raises(ResourceNotFoundError) as exc_info: email_client.inbound.delete(delete_request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) or "could not be found" in str(exc_info.value).lower() + assert ( + "not found" in str(exc_info.value).lower() + or "404" in str(exc_info.value) + or "could not be found" in str(exc_info.value).lower() + ) @vcr.use_cassette("inbound_validation_error.yaml") def test_list_inbound_routes_validation_error(self, email_client): @@ -222,10 +232,7 @@ def test_list_inbound_routes_validation_error(self, email_client): # Should raise an AttributeError for invalid request type error_str = str(exc_info.value).lower() - assert ( - "attribute" in error_str - or "to_query_params" in error_str - ) + assert "attribute" in error_str or "to_query_params" in error_str @vcr.use_cassette("inbound_api_response_structure.yaml") def test_api_response_structure(self, email_client, basic_inbound_list_request): @@ -249,7 +256,9 @@ def test_api_response_structure(self, email_client, basic_inbound_list_request): assert len(response.request_id) > 0 @vcr.use_cassette("inbound_empty_result.yaml") - def test_list_inbound_routes_empty_result(self, email_client, basic_inbound_list_request): + def test_list_inbound_routes_empty_result( + self, email_client, basic_inbound_list_request + ): """Test listing inbound routes when no routes exist.""" response = email_client.inbound.list(basic_inbound_list_request) @@ -271,7 +280,7 @@ def test_create_inbound_route_model_validation(self): domain_enabled=False, catch_filter=InboundFilterGroup(type="catch_all"), match_filter=InboundFilterGroup(type="match_all"), - forwards=[InboundForward(type="email", value="test@example.com")] + forwards=[InboundForward(type="email", value="test@example.com")], ) assert "domain id is required" in str(exc_info.value).lower() @@ -283,7 +292,7 @@ def test_create_inbound_route_model_validation(self): domain_enabled=False, catch_filter=InboundFilterGroup(type="catch_all"), match_filter=InboundFilterGroup(type="match_all"), - forwards=[InboundForward(type="email", value="test@example.com")] + forwards=[InboundForward(type="email", value="test@example.com")], ) assert "name is required" in str(exc_info.value).lower() @@ -295,7 +304,7 @@ def test_create_inbound_route_model_validation(self): domain_enabled=False, catch_filter=InboundFilterGroup(type="catch_all"), match_filter=InboundFilterGroup(type="match_all"), - forwards=[] + forwards=[], ) assert "at least one forward is required" in str(exc_info.value).lower() @@ -307,9 +316,12 @@ def test_create_inbound_route_model_validation(self): domain_enabled=True, catch_filter=InboundFilterGroup(type="catch_all"), match_filter=InboundFilterGroup(type="match_all"), - forwards=[InboundForward(type="email", value="test@example.com")] + forwards=[InboundForward(type="email", value="test@example.com")], ) - assert "inbound domain is required when domain is enabled" in str(exc_info.value).lower() + assert ( + "inbound domain is required when domain is enabled" + in str(exc_info.value).lower() + ) # Test domain enabled without inbound_priority with pytest.raises(ValueError) as exc_info: @@ -320,9 +332,12 @@ def test_create_inbound_route_model_validation(self): inbound_domain="test.com", catch_filter=InboundFilterGroup(type="catch_all"), match_filter=InboundFilterGroup(type="match_all"), - forwards=[InboundForward(type="email", value="test@example.com")] + forwards=[InboundForward(type="email", value="test@example.com")], ) - assert "inbound priority is required when domain is enabled" in str(exc_info.value).lower() + assert ( + "inbound priority is required when domain is enabled" + in str(exc_info.value).lower() + ) class TestInboundBuilderIntegration: @@ -333,9 +348,9 @@ def test_builder_list_basic_usage(self, email_client): """Test basic inbound list using builder.""" builder = InboundBuilder() request = builder.page(1).limit(10).build_list_request() - + response = email_client.inbound.list(request) - + assert isinstance(response, APIResponse) assert response.status_code == 200 @@ -343,10 +358,12 @@ def test_builder_list_basic_usage(self, email_client): def test_builder_list_with_domain_filter(self, email_client, test_domain_id): """Test inbound list with domain filter using builder.""" builder = InboundBuilder() - request = builder.page(1).limit(10).domain_id(test_domain_id).build_list_request() - + request = ( + builder.page(1).limit(10).domain_id(test_domain_id).build_list_request() + ) + response = email_client.inbound.list(request) - + assert isinstance(response, APIResponse) assert response.status_code == 200 @@ -354,10 +371,10 @@ def test_builder_list_with_domain_filter(self, email_client, test_domain_id): def test_builder_get_not_found(self, email_client): """Test getting non-existent inbound route using builder.""" from mailersend.exceptions import ResourceNotFoundError - + builder = InboundBuilder() request = builder.inbound_id("test-inbound-id").build_get_request() - + with pytest.raises(ResourceNotFoundError): email_client.inbound.get(request) @@ -365,16 +382,17 @@ def test_builder_get_not_found(self, email_client): def test_builder_create_invalid_domain(self, email_client): """Test creating inbound route with invalid domain using builder.""" from mailersend.exceptions import BadRequestError - + builder = InboundBuilder() - request = (builder - .domain_id("invalid-domain-id") + request = ( + builder.domain_id("invalid-domain-id") .name("Test Route") .domain_enabled(False) .catch_all() .match_all() .add_email_forward("test@example.com") - .build_create_request()) - + .build_create_request() + ) + with pytest.raises(BadRequestError): - email_client.inbound.create(request) \ No newline at end of file + email_client.inbound.create(request) diff --git a/tests/integration/test_messages.py b/tests/integration/test_messages.py index e32c5e4..c87d5c3 100644 --- a/tests/integration/test_messages.py +++ b/tests/integration/test_messages.py @@ -14,9 +14,7 @@ @pytest.fixture def basic_messages_list_request(): """Basic messages list request""" - return MessagesListRequest( - query_params=MessagesListQueryParams(page=1, limit=10) - ) + return MessagesListRequest(query_params=MessagesListQueryParams(page=1, limit=10)) @pytest.fixture @@ -97,11 +95,15 @@ def test_list_messages_different_limit(self, email_client): def test_get_message_not_found(self, email_client, message_get_request): """Test getting a non-existent message returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.messages.get_message(message_get_request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) or "could not be found" in str(exc_info.value).lower() + assert ( + "not found" in str(exc_info.value).lower() + or "404" in str(exc_info.value) + or "could not be found" in str(exc_info.value).lower() + ) @vcr.use_cassette("messages_validation_error.yaml") def test_list_messages_validation_error(self, email_client): @@ -112,10 +114,7 @@ def test_list_messages_validation_error(self, email_client): # Should raise an AttributeError for invalid request type error_str = str(exc_info.value).lower() - assert ( - "attribute" in error_str - or "to_query_params" in error_str - ) + assert "attribute" in error_str or "to_query_params" in error_str @vcr.use_cassette("messages_api_response_structure.yaml") def test_api_response_structure(self, email_client, basic_messages_list_request): @@ -139,7 +138,9 @@ def test_api_response_structure(self, email_client, basic_messages_list_request) assert len(response.request_id) > 0 @vcr.use_cassette("messages_empty_result.yaml") - def test_list_messages_empty_result(self, email_client, basic_messages_list_request): + def test_list_messages_empty_result( + self, email_client, basic_messages_list_request + ): """Test listing messages when no messages exist.""" response = email_client.messages.list_messages(basic_messages_list_request) @@ -160,6 +161,7 @@ def test_message_get_model_validation(self): # Test None message_id - this will raise a pydantic ValidationError from pydantic import ValidationError + with pytest.raises(ValidationError) as exc_info: MessageGetRequest(message_id=None) assert "input should be a valid string" in str(exc_info.value).lower() @@ -170,15 +172,15 @@ def test_messages_list_query_params_validation(self): params = MessagesListQueryParams(page=1, limit=25) assert params.page == 1 assert params.limit == 25 - + # Test minimum limit validation with pytest.raises(ValueError): MessagesListQueryParams(limit=5) # Below minimum of 10 - - # Test maximum limit validation + + # Test maximum limit validation with pytest.raises(ValueError): MessagesListQueryParams(limit=150) # Above maximum of 100 - + # Test minimum page validation with pytest.raises(ValueError): MessagesListQueryParams(page=0) # Below minimum of 1 @@ -192,9 +194,9 @@ def test_builder_list_basic_usage(self, email_client): """Test basic messages list using builder.""" builder = MessagesBuilder() request = builder.page(1).limit(10).build_list_request() - + response = email_client.messages.list_messages(request) - + assert isinstance(response, APIResponse) assert response.status_code == 200 @@ -203,12 +205,12 @@ def test_builder_list_with_custom_limit(self, email_client): """Test messages list with custom limit using builder.""" builder = MessagesBuilder() request = builder.page(1).limit(50).build_list_request() - + response = email_client.messages.list_messages(request) - + assert isinstance(response, APIResponse) assert response.status_code == 200 - + # Check that the limit was applied if "meta" in response.data: meta = response.data["meta"] @@ -218,10 +220,10 @@ def test_builder_list_with_custom_limit(self, email_client): def test_builder_get_not_found(self, email_client): """Test getting non-existent message using builder.""" from mailersend.exceptions import ResourceNotFoundError - + builder = MessagesBuilder() request = builder.message_id("test-message-id").build_get_request() - + with pytest.raises(ResourceNotFoundError): email_client.messages.get_message(request) @@ -229,17 +231,18 @@ def test_builder_reset_functionality(self): """Test builder reset functionality.""" builder = MessagesBuilder() builder.page(2).limit(50).message_id("test-id") - + # Reset the builder builder.reset() - + # Build a basic request to verify reset worked request = builder.build_list_request() assert request.query_params.page == 1 # Default value assert request.query_params.limit == 25 # Default value - + # Verify message_id was reset from mailersend.exceptions import ValidationError + with pytest.raises(ValidationError): builder.build_get_request() # Should fail because message_id was reset @@ -247,17 +250,17 @@ def test_builder_copy_functionality(self): """Test builder copy functionality.""" original_builder = MessagesBuilder() original_builder.page(2).limit(50) - + # Copy the builder copied_builder = original_builder.copy() - + # Modify the copy copied_builder.page(3) - + # Verify original is unchanged original_request = original_builder.build_list_request() copied_request = copied_builder.build_list_request() - + assert original_request.query_params.page == 2 assert copied_request.query_params.page == 3 assert original_request.query_params.limit == copied_request.query_params.limit @@ -265,17 +268,17 @@ def test_builder_copy_functionality(self): def test_builder_fluent_interface(self): """Test that builder methods return self for chaining.""" builder = MessagesBuilder() - + # Test method chaining result = builder.page(1).limit(10).message_id("test-id") - + assert result is builder - + # Verify the builder state for list request list_request = builder.build_list_request() assert list_request.query_params.page == 1 assert list_request.query_params.limit == 10 - + # Verify the builder state for get request get_request = builder.build_get_request() assert get_request.message_id == "test-id" @@ -283,29 +286,29 @@ def test_builder_fluent_interface(self): def test_builder_validation_errors(self): """Test builder validation for invalid inputs.""" from mailersend.exceptions import ValidationError - + builder = MessagesBuilder() - + # Test invalid page with pytest.raises(ValidationError) as exc_info: builder.page(0) assert "page must be greater than 0" in str(exc_info.value).lower() - + # Test invalid limit (too low) with pytest.raises(ValidationError) as exc_info: builder.limit(5) assert "limit must be between 10 and 100" in str(exc_info.value).lower() - + # Test invalid limit (too high) with pytest.raises(ValidationError) as exc_info: builder.limit(150) assert "limit must be between 10 and 100" in str(exc_info.value).lower() - + # Test empty message_id with pytest.raises(ValidationError) as exc_info: builder.message_id("") assert "message id cannot be empty" in str(exc_info.value).lower() - + # Test building get request without message_id fresh_builder = MessagesBuilder() with pytest.raises(ValidationError) as exc_info: @@ -315,10 +318,10 @@ def test_builder_validation_errors(self): def test_builder_default_values(self): """Test that builder uses appropriate default values.""" builder = MessagesBuilder() - + # Build request without setting any values request = builder.build_list_request() - + # Should use default values from the model assert request.query_params.page == 1 assert request.query_params.limit == 25 @@ -326,51 +329,53 @@ def test_builder_default_values(self): def test_builder_pagination_scenarios(self): """Test various pagination scenarios with builder.""" builder = MessagesBuilder() - + # Test first page request1 = builder.page(1).limit(10).build_list_request() assert request1.query_params.page == 1 assert request1.query_params.limit == 10 - + # Test different page request2 = builder.page(5).limit(20).build_list_request() assert request2.query_params.page == 5 assert request2.query_params.limit == 20 - + # Test maximum limit request3 = builder.page(1).limit(100).build_list_request() assert request3.query_params.page == 1 assert request3.query_params.limit == 100 - @vcr.use_cassette("messages_comprehensive_workflow.yaml") + @vcr.use_cassette("messages_comprehensive_workflow.yaml") def test_comprehensive_messages_workflow(self, email_client): """Test comprehensive workflow covering list, get, error scenarios, and builder usage.""" # Test list with different pagination settings list_request = MessagesListRequest( query_params=MessagesListQueryParams(page=1, limit=10) ) - + response = email_client.messages.list_messages(list_request) assert isinstance(response, APIResponse) assert response.status_code == 200 - + # Test builder pattern with different configurations builder = MessagesBuilder() - + # Test list with builder builder_request = builder.page(1).limit(25).build_list_request() builder_response = email_client.messages.list_messages(builder_request) assert isinstance(builder_response, APIResponse) assert builder_response.status_code == 200 - + # Test error scenarios from mailersend.exceptions import ResourceNotFoundError - + get_request = MessageGetRequest(message_id="non-existent-id") with pytest.raises(ResourceNotFoundError): email_client.messages.get_message(get_request) - + # Test builder get error scenario - builder_get_request = builder.message_id("another-non-existent-id").build_get_request() + builder_get_request = builder.message_id( + "another-non-existent-id" + ).build_get_request() with pytest.raises(ResourceNotFoundError): - email_client.messages.get_message(builder_get_request) \ No newline at end of file + email_client.messages.get_message(builder_get_request) diff --git a/tests/integration/test_recipients.py b/tests/integration/test_recipients.py index b7c6b06..72ed25f 100644 --- a/tests/integration/test_recipients.py +++ b/tests/integration/test_recipients.py @@ -93,20 +93,26 @@ def test_list_recipients_with_pagination(self, email_client): assert meta["current_page"] == 1 @vcr.use_cassette("recipients_get_single.yaml") - def test_get_recipient_not_found_with_test_id(self, email_client, recipient_get_request): + def test_get_recipient_not_found_with_test_id( + self, email_client, recipient_get_request + ): """Test getting a non-existent recipient returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.recipients.get_recipient(recipient_get_request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) + assert "not found" in str(exc_info.value).lower() or "404" in str( + exc_info.value + ) @vcr.use_cassette("recipients_delete_success.yaml") - def test_delete_recipient_not_found_with_test_id(self, email_client, recipient_get_request): + def test_delete_recipient_not_found_with_test_id( + self, email_client, recipient_get_request + ): """Test deleting a non-existent recipient returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + delete_request = RecipientDeleteRequest( recipient_id=recipient_get_request.recipient_id ) @@ -114,7 +120,9 @@ def test_delete_recipient_not_found_with_test_id(self, email_client, recipient_g with pytest.raises(ResourceNotFoundError) as exc_info: email_client.recipients.delete_recipient(delete_request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) + assert "not found" in str(exc_info.value).lower() or "404" in str( + exc_info.value + ) # Suppression Lists Tests @@ -165,7 +173,9 @@ def test_get_hard_bounces_basic(self, email_client, suppression_list_request): @vcr.use_cassette("recipients_spam_complaints_basic.yaml") def test_get_spam_complaints_basic(self, email_client, suppression_list_request): """Test getting spam complaints with basic parameters.""" - response = email_client.recipients.list_spam_complaints(suppression_list_request) + response = email_client.recipients.list_spam_complaints( + suppression_list_request + ) assert isinstance(response, APIResponse) assert response.status_code == 200 @@ -209,7 +219,7 @@ def test_add_to_blocklist_invalid_domain(self, email_client): """Test adding to blocklist with invalid domain ID returns 422.""" from mailersend.exceptions import BadRequestError from mailersend.models.recipients import SuppressionAddRequest - + request = SuppressionAddRequest( domain_id="test-domain-id", # Invalid domain ID recipients=["test@example.com"], @@ -219,14 +229,16 @@ def test_add_to_blocklist_invalid_domain(self, email_client): email_client.recipients.add_to_blocklist(request) error_str = str(exc_info.value).lower() - assert "domain" in error_str and ("invalid" in error_str or "required" in error_str) + assert "domain" in error_str and ( + "invalid" in error_str or "required" in error_str + ) @vcr.use_cassette("recipients_add_hard_bounces.yaml") def test_add_hard_bounces_invalid_domain(self, email_client): """Test adding hard bounces with invalid domain ID returns 422.""" from mailersend.exceptions import BadRequestError from mailersend.models.recipients import SuppressionAddRequest - + request = SuppressionAddRequest( domain_id="test-domain-id", # Invalid domain ID recipients=["test@example.com"], @@ -236,14 +248,16 @@ def test_add_hard_bounces_invalid_domain(self, email_client): email_client.recipients.add_hard_bounces(request) error_str = str(exc_info.value).lower() - assert "domain" in error_str and ("invalid" in error_str or "required" in error_str) + assert "domain" in error_str and ( + "invalid" in error_str or "required" in error_str + ) @vcr.use_cassette("recipients_add_spam_complaints.yaml") def test_add_spam_complaints_invalid_domain(self, email_client): """Test adding spam complaints with invalid domain ID returns 422.""" from mailersend.exceptions import BadRequestError from mailersend.models.recipients import SuppressionAddRequest - + request = SuppressionAddRequest( domain_id="test-domain-id", # Invalid domain ID recipients=["test@example.com"], @@ -253,14 +267,16 @@ def test_add_spam_complaints_invalid_domain(self, email_client): email_client.recipients.add_spam_complaints(request) error_str = str(exc_info.value).lower() - assert "domain" in error_str and ("invalid" in error_str or "required" in error_str) + assert "domain" in error_str and ( + "invalid" in error_str or "required" in error_str + ) @vcr.use_cassette("recipients_add_unsubscribes.yaml") def test_add_unsubscribes_invalid_domain(self, email_client): """Test adding unsubscribes with invalid domain ID returns 422.""" from mailersend.exceptions import BadRequestError from mailersend.models.recipients import SuppressionAddRequest - + request = SuppressionAddRequest( domain_id="test-domain-id", # Invalid domain ID recipients=["test@example.com"], @@ -270,14 +286,16 @@ def test_add_unsubscribes_invalid_domain(self, email_client): email_client.recipients.add_unsubscribes(request) error_str = str(exc_info.value).lower() - assert "domain" in error_str and ("invalid" in error_str or "required" in error_str) + assert "domain" in error_str and ( + "invalid" in error_str or "required" in error_str + ) @vcr.use_cassette("recipients_delete_from_blocklist.yaml") def test_delete_from_blocklist_invalid_domain(self, email_client): """Test deleting from blocklist with invalid domain ID returns 422.""" from mailersend.exceptions import BadRequestError from mailersend.models.recipients import SuppressionDeleteRequest - + request = SuppressionDeleteRequest( domain_id="test-domain-id", # Invalid domain ID ids=["test-id"], @@ -287,14 +305,16 @@ def test_delete_from_blocklist_invalid_domain(self, email_client): email_client.recipients.delete_from_blocklist(request) error_str = str(exc_info.value).lower() - assert "domain" in error_str and ("invalid" in error_str or "required" in error_str) + assert "domain" in error_str and ( + "invalid" in error_str or "required" in error_str + ) @vcr.use_cassette("recipients_comprehensive_workflow.yaml") def test_comprehensive_recipients_workflow_invalid_domain(self, email_client): """Test comprehensive workflow with invalid domain ID returns errors.""" from mailersend.exceptions import BadRequestError from mailersend.models.recipients import SuppressionAddRequest - + # Step 1: Try to add recipient to blocklist (should fail with invalid domain) add_request = SuppressionAddRequest( domain_id="test-domain-id", # Invalid domain ID @@ -305,7 +325,9 @@ def test_comprehensive_recipients_workflow_invalid_domain(self, email_client): email_client.recipients.add_to_blocklist(add_request) error_str = str(exc_info.value).lower() - assert "domain" in error_str and ("invalid" in error_str or "required" in error_str) + assert "domain" in error_str and ( + "invalid" in error_str or "required" in error_str + ) @vcr.use_cassette("recipients_api_response_structure.yaml") def test_api_response_structure(self, email_client, basic_recipients_list_request): @@ -337,10 +359,7 @@ def test_list_recipients_validation_error(self, email_client): # Should raise an AttributeError for invalid request type error_str = str(exc_info.value).lower() - assert ( - "attribute" in error_str - or "to_query_params" in error_str - ) + assert "attribute" in error_str or "to_query_params" in error_str @vcr.use_cassette("recipients_empty_list.yaml") def test_list_recipients_empty_result( diff --git a/tests/integration/test_schedules.py b/tests/integration/test_schedules.py index ff1f340..f382938 100644 --- a/tests/integration/test_schedules.py +++ b/tests/integration/test_schedules.py @@ -14,9 +14,7 @@ @pytest.fixture def basic_schedules_list_request(): """Basic schedules list request""" - return SchedulesListRequest( - query_params=SchedulesListQueryParams(page=1, limit=10) - ) + return SchedulesListRequest(query_params=SchedulesListQueryParams(page=1, limit=10)) @pytest.fixture @@ -52,7 +50,9 @@ def test_list_schedules_basic(self, email_client, basic_schedules_list_request): if schedules: first_schedule = schedules[0] assert "message_id" in first_schedule - assert "subject" in first_schedule or "send_at" in first_schedule # API may not always include subject + assert ( + "subject" in first_schedule or "send_at" in first_schedule + ) # API may not always include subject assert "send_at" in first_schedule assert "status" in first_schedule assert "created_at" in first_schedule @@ -83,9 +83,7 @@ def test_list_schedules_with_domain_filter(self, email_client, sample_domain_id) """Test listing scheduled messages filtered by domain.""" request = SchedulesListRequest( query_params=SchedulesListQueryParams( - page=1, - limit=10, - domain_id=sample_domain_id + page=1, limit=10, domain_id=sample_domain_id ) ) @@ -99,11 +97,7 @@ def test_list_schedules_with_domain_filter(self, email_client, sample_domain_id) def test_list_schedules_with_status_filter(self, email_client): """Test listing scheduled messages filtered by status.""" request = SchedulesListRequest( - query_params=SchedulesListQueryParams( - page=1, - limit=10, - status="scheduled" - ) + query_params=SchedulesListQueryParams(page=1, limit=10, status="scheduled") ) response = email_client.schedules.list_schedules(request) @@ -119,21 +113,29 @@ def test_list_schedules_with_status_filter(self, email_client): assert schedule["status"] == "scheduled" @vcr.use_cassette("schedules_get_single.yaml") - def test_get_schedule_not_found_with_test_id(self, email_client, schedule_get_request): + def test_get_schedule_not_found_with_test_id( + self, email_client, schedule_get_request + ): """Test getting a non-existent scheduled message returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.schedules.get_schedule(schedule_get_request) error_str = str(exc_info.value).lower() - assert "not found" in error_str or "404" in error_str or "could not be found" in error_str + assert ( + "not found" in error_str + or "404" in error_str + or "could not be found" in error_str + ) @vcr.use_cassette("schedules_delete.yaml") - def test_delete_schedule_not_found_with_test_id(self, email_client, schedule_get_request): + def test_delete_schedule_not_found_with_test_id( + self, email_client, schedule_get_request + ): """Test deleting a non-existent scheduled message returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + delete_request = ScheduleDeleteRequest( message_id=schedule_get_request.message_id ) @@ -142,10 +144,13 @@ def test_delete_schedule_not_found_with_test_id(self, email_client, schedule_get email_client.schedules.delete_schedule(delete_request) error_str = str(exc_info.value).lower() - assert ("not found" in error_str or "404" in error_str or - "could not be found" in error_str or - "not possible to delete" in error_str or - "already been sent" in error_str) + assert ( + "not found" in error_str + or "404" in error_str + or "could not be found" in error_str + or "not possible to delete" in error_str + or "already been sent" in error_str + ) @vcr.use_cassette("schedules_validation_error.yaml") def test_list_schedules_validation_error(self, email_client): @@ -156,10 +161,7 @@ def test_list_schedules_validation_error(self, email_client): # Should raise an AttributeError for invalid request type error_str = str(exc_info.value).lower() - assert ( - "attribute" in error_str - or "to_query_params" in error_str - ) + assert "attribute" in error_str or "to_query_params" in error_str @vcr.use_cassette("schedules_api_response_structure.yaml") def test_api_response_structure(self, email_client, basic_schedules_list_request): @@ -183,7 +185,9 @@ def test_api_response_structure(self, email_client, basic_schedules_list_request assert len(response.request_id) > 0 @vcr.use_cassette("schedules_empty_result.yaml") - def test_list_schedules_empty_result(self, email_client, basic_schedules_list_request): + def test_list_schedules_empty_result( + self, email_client, basic_schedules_list_request + ): """Test listing scheduled messages when no schedules exist.""" response = email_client.schedules.list_schedules(basic_schedules_list_request) @@ -223,33 +227,30 @@ def test_schedules_list_query_params_validation(self): """Test validation for schedules list query parameters.""" # Test valid parameters params = SchedulesListQueryParams( - page=1, - limit=25, - domain_id="test-domain", - status="scheduled" + page=1, limit=25, domain_id="test-domain", status="scheduled" ) assert params.page == 1 assert params.limit == 25 assert params.domain_id == "test-domain" assert params.status == "scheduled" - + # Test minimum limit validation with pytest.raises(ValueError): SchedulesListQueryParams(limit=5) # Below minimum of 10 - - # Test maximum limit validation + + # Test maximum limit validation with pytest.raises(ValueError): SchedulesListQueryParams(limit=150) # Above maximum of 100 - + # Test minimum page validation with pytest.raises(ValueError): SchedulesListQueryParams(page=0) # Below minimum of 1 - + # Test empty domain_id validation with pytest.raises(ValueError) as exc_info: SchedulesListQueryParams(domain_id="") assert "domain id cannot be empty" in str(exc_info.value).lower() - + # Test invalid status validation with pytest.raises(ValueError): SchedulesListQueryParams(status="invalid") # Not in allowed values @@ -258,23 +259,20 @@ def test_schedules_list_query_params_to_dict(self): """Test query parameters conversion to dictionary.""" # Test with all parameters params = SchedulesListQueryParams( - page=2, - limit=50, - domain_id="test-domain", - status="sent" + page=2, limit=50, domain_id="test-domain", status="sent" ) query_dict = params.to_query_params() - + assert query_dict["page"] == 2 assert query_dict["limit"] == 50 assert query_dict["domain_id"] == "test-domain" assert query_dict["status"] == "sent" - + # Test with minimal parameters (only defaults) params_minimal = SchedulesListQueryParams() query_dict_minimal = params_minimal.to_query_params() - + assert query_dict_minimal["page"] == 1 assert query_dict_minimal["limit"] == 25 assert "domain_id" not in query_dict_minimal # None values excluded - assert "status" not in query_dict_minimal # None values excluded \ No newline at end of file + assert "status" not in query_dict_minimal # None values excluded diff --git a/tests/integration/test_sms_activity.py b/tests/integration/test_sms_activity.py index e308ac9..8a79916 100644 --- a/tests/integration/test_sms_activity.py +++ b/tests/integration/test_sms_activity.py @@ -33,7 +33,9 @@ class TestSmsActivityIntegration: """Integration tests for SMS Activity API.""" @vcr.use_cassette("sms_activity_list_basic.yaml") - def test_list_sms_activity_basic(self, email_client, basic_sms_activity_list_request): + def test_list_sms_activity_basic( + self, email_client, basic_sms_activity_list_request + ): """Test listing SMS activity with basic parameters.""" response = email_client.sms_activity.list(basic_sms_activity_list_request) @@ -75,14 +77,14 @@ def test_list_sms_activity_with_pagination(self, email_client): assert meta["current_page"] == 1 @vcr.use_cassette("sms_activity_list_with_sms_number_filter.yaml") - def test_list_sms_activity_with_sms_number_filter(self, email_client, test_sms_number_id): + def test_list_sms_activity_with_sms_number_filter( + self, email_client, test_sms_number_id + ): """Test listing SMS activity with SMS number ID filter.""" from mailersend.exceptions import BadRequestError - + request = SmsActivityListRequest( - sms_number_id=test_sms_number_id, - page=1, - limit=10 + sms_number_id=test_sms_number_id, page=1, limit=10 ) try: @@ -98,17 +100,14 @@ def test_list_sms_activity_with_sms_number_filter(self, email_client, test_sms_n def test_list_sms_activity_with_date_range(self, email_client): """Test listing SMS activity with date range filter.""" from mailersend.exceptions import BadRequestError - + # Use fixed timestamps to ensure VCR cassette matching # These represent a recent 7-day period date_to = 1753800000 # Fixed timestamp date_from = date_to - (7 * 24 * 60 * 60) # 7 days earlier - + request = SmsActivityListRequest( - date_from=date_from, - date_to=date_to, - page=1, - limit=10 + date_from=date_from, date_to=date_to, page=1, limit=10 ) try: @@ -118,16 +117,16 @@ def test_list_sms_activity_with_date_range(self, email_client): assert response.data is not None except BadRequestError as e: # Expected if date range exceeds account retention limit - assert "date" in str(e).lower() or "retention" in str(e).lower() or "range" in str(e).lower() + assert ( + "date" in str(e).lower() + or "retention" in str(e).lower() + or "range" in str(e).lower() + ) @vcr.use_cassette("sms_activity_list_with_status_filter.yaml") def test_list_sms_activity_with_status_filter(self, email_client): """Test listing SMS activity with status filter.""" - request = SmsActivityListRequest( - status=["queued", "sent"], - page=1, - limit=10 - ) + request = SmsActivityListRequest(status=["queued", "sent"], page=1, limit=10) response = email_client.sms_activity.list(request) @@ -144,11 +143,15 @@ def test_list_sms_activity_with_status_filter(self, email_client): def test_get_sms_message_not_found(self, email_client, sms_message_get_request): """Test getting a non-existent SMS message returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.sms_activity.get(sms_message_get_request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) or "could not be found" in str(exc_info.value).lower() + assert ( + "not found" in str(exc_info.value).lower() + or "404" in str(exc_info.value) + or "could not be found" in str(exc_info.value).lower() + ) @vcr.use_cassette("sms_activity_validation_error.yaml") def test_list_sms_activity_validation_error(self, email_client): @@ -159,13 +162,12 @@ def test_list_sms_activity_validation_error(self, email_client): # Should raise an AttributeError for invalid request type error_str = str(exc_info.value).lower() - assert ( - "attribute" in error_str - or "to_query_params" in error_str - ) + assert "attribute" in error_str or "to_query_params" in error_str @vcr.use_cassette("sms_activity_api_response_structure.yaml") - def test_api_response_structure(self, email_client, basic_sms_activity_list_request): + def test_api_response_structure( + self, email_client, basic_sms_activity_list_request + ): """Test that API response has the expected structure and metadata.""" response = email_client.sms_activity.list(basic_sms_activity_list_request) @@ -186,7 +188,9 @@ def test_api_response_structure(self, email_client, basic_sms_activity_list_requ assert len(response.request_id) > 0 @vcr.use_cassette("sms_activity_empty_result.yaml") - def test_list_sms_activity_empty_result(self, email_client, basic_sms_activity_list_request): + def test_list_sms_activity_empty_result( + self, email_client, basic_sms_activity_list_request + ): """Test listing SMS activity when no activities exist.""" response = email_client.sms_activity.list(basic_sms_activity_list_request) @@ -207,9 +211,9 @@ def test_builder_list_basic_usage(self, email_client): """Test basic SMS activity list using builder.""" builder = SmsActivityBuilder() request = builder.page(1).limit(10).build_list_request() - + response = email_client.sms_activity.list(request) - + assert isinstance(response, APIResponse) assert response.status_code == 200 @@ -217,41 +221,49 @@ def test_builder_list_basic_usage(self, email_client): def test_builder_list_with_filters(self, email_client): """Test SMS activity list with various filters using builder.""" from mailersend.exceptions import BadRequestError - + builder = SmsActivityBuilder() - + # Use fixed timestamps to ensure VCR cassette matching date_to = 1753800000 # Fixed timestamp date_from = date_to - (7 * 24 * 60 * 60) # 7 days earlier - - request = (builder - .date_from(date_from) + + request = ( + builder.date_from(date_from) .date_to(date_to) .status(["sent", "delivered"]) .page(1) .limit(25) - .build_list_request()) - + .build_list_request() + ) + try: response = email_client.sms_activity.list(request) assert isinstance(response, APIResponse) assert response.status_code == 200 except BadRequestError as e: # Expected if date range exceeds account retention limit - assert "date" in str(e).lower() or "retention" in str(e).lower() or "range" in str(e).lower() + assert ( + "date" in str(e).lower() + or "retention" in str(e).lower() + or "range" in str(e).lower() + ) @vcr.use_cassette("sms_activity_builder_list_with_sms_number.yaml") - def test_builder_list_with_sms_number_filter(self, email_client, test_sms_number_id): + def test_builder_list_with_sms_number_filter( + self, email_client, test_sms_number_id + ): """Test SMS activity list with SMS number filter using builder.""" from mailersend.exceptions import BadRequestError - + builder = SmsActivityBuilder() - request = (builder - .sms_number_id(test_sms_number_id) + request = ( + builder.sms_number_id(test_sms_number_id) .page(1) .limit(10) - .build_list_request()) - + .build_list_request() + ) + try: response = email_client.sms_activity.list(request) assert isinstance(response, APIResponse) @@ -264,31 +276,32 @@ def test_builder_list_with_sms_number_filter(self, email_client, test_sms_number def test_builder_get_not_found(self, email_client): """Test getting non-existent SMS message using builder.""" from mailersend.exceptions import ResourceNotFoundError - + builder = SmsActivityBuilder() request = builder.sms_message_id("test-sms-message-id").build_get_request() - + with pytest.raises(ResourceNotFoundError): email_client.sms_activity.get(request) def test_builder_fluent_interface(self): """Test that builder methods return self for chaining.""" builder = SmsActivityBuilder() - + current_time = int(time.time()) - + # Test method chaining - result = (builder - .page(1) + result = ( + builder.page(1) .limit(10) .sms_number_id("test-sms-number") .date_from(current_time - 86400) .date_to(current_time) .status(["sent", "delivered"]) - .sms_message_id("test-message")) - + .sms_message_id("test-message") + ) + assert result is builder - + # Verify the builder state for different requests list_request = builder.build_list_request() assert list_request.page == 1 @@ -297,7 +310,7 @@ def test_builder_fluent_interface(self): assert list_request.date_from == current_time - 86400 assert list_request.date_to == current_time assert list_request.status == ["sent", "delivered"] - + get_request = builder.build_get_request() assert get_request.sms_message_id == "test-message" @@ -305,10 +318,10 @@ def test_builder_reset_functionality(self): """Test builder reset functionality.""" builder = SmsActivityBuilder() builder.page(2).limit(50).sms_number_id("test").status(["sent"]) - + # Reset the builder builder.reset() - + # Verify all fields are cleared assert builder._page is None assert builder._limit is None @@ -321,53 +334,58 @@ def test_builder_reset_functionality(self): def test_builder_validation_errors(self): """Test builder validation for missing required fields.""" builder = SmsActivityBuilder() - + # Test building get request without SMS message ID with pytest.raises(ValueError) as exc_info: builder.build_get_request() assert "sms message id must be set" in str(exc_info.value).lower() @vcr.use_cassette("sms_activity_comprehensive_workflow.yaml") - def test_comprehensive_sms_activity_workflow(self, email_client, test_sms_number_id): + def test_comprehensive_sms_activity_workflow( + self, email_client, test_sms_number_id + ): """Test comprehensive workflow covering list operations, error scenarios, and builder usage.""" # Test list with different configurations list_request = SmsActivityListRequest(page=1, limit=10) - + response = email_client.sms_activity.list(list_request) assert isinstance(response, APIResponse) assert response.status_code == 200 - + # Test builder pattern with different configurations builder = SmsActivityBuilder() - + # Test list with builder and filters date_to = 1753800000 # Fixed timestamp date_from = date_to - (7 * 24 * 60 * 60) # 7 days earlier - + try: - builder_request = (builder - .date_from(date_from) + builder_request = ( + builder.date_from(date_from) .date_to(date_to) .status(["sent"]) .page(1) .limit(25) - .build_list_request()) + .build_list_request() + ) builder_response = email_client.sms_activity.list(builder_request) assert isinstance(builder_response, APIResponse) assert builder_response.status_code == 200 except Exception: # Handle potential API errors in test environment pass - + # Test error scenarios from mailersend.exceptions import ResourceNotFoundError - + # Test get non-existent SMS message get_request = SmsMessageGetRequest(sms_message_id="non-existent-id") with pytest.raises(ResourceNotFoundError): email_client.sms_activity.get(get_request) - + # Test builder error scenarios - builder_get_request = builder.sms_message_id("another-non-existent-id").build_get_request() + builder_get_request = builder.sms_message_id( + "another-non-existent-id" + ).build_get_request() with pytest.raises(ResourceNotFoundError): - email_client.sms_activity.get(builder_get_request) \ No newline at end of file + email_client.sms_activity.get(builder_get_request) diff --git a/tests/integration/test_sms_messages.py b/tests/integration/test_sms_messages.py index bd8e7c8..2a4d4aa 100644 --- a/tests/integration/test_sms_messages.py +++ b/tests/integration/test_sms_messages.py @@ -86,14 +86,18 @@ def test_list_sms_messages_empty_result(self, email_client, basic_sms_list_reque assert response.data["data"] == [] @vcr.use_cassette("sms_messages_get_single.yaml") - def test_get_sms_message_not_found_with_test_id(self, email_client, sms_message_get_request): + def test_get_sms_message_not_found_with_test_id( + self, email_client, sms_message_get_request + ): """Test getting a non-existent SMS message returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.sms_messages.get_sms_message(sms_message_get_request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) + assert "not found" in str(exc_info.value).lower() or "404" in str( + exc_info.value + ) @vcr.use_cassette("sms_messages_list_with_filters.yaml") def test_list_sms_messages_with_filters(self, email_client): @@ -126,10 +130,7 @@ def test_list_sms_messages_validation_error(self, email_client): # Should raise an AttributeError for invalid request type error_str = str(exc_info.value).lower() - assert ( - "attribute" in error_str - or "to_query_params" in error_str - ) + assert "attribute" in error_str or "to_query_params" in error_str def test_get_sms_message_validation_error(self, email_client): """Test that invalid request raises validation error.""" @@ -139,10 +140,7 @@ def test_get_sms_message_validation_error(self, email_client): # Should raise an AttributeError for invalid request type error_str = str(exc_info.value).lower() - assert ( - "attribute" in error_str - or "sms_message_id" in error_str - ) + assert "attribute" in error_str or "sms_message_id" in error_str @vcr.use_cassette("sms_messages_api_response_structure.yaml") def test_api_response_structure(self, email_client, basic_sms_list_request): diff --git a/tests/integration/test_sms_numbers.py b/tests/integration/test_sms_numbers.py index 639ca9b..71f83f7 100644 --- a/tests/integration/test_sms_numbers.py +++ b/tests/integration/test_sms_numbers.py @@ -20,11 +20,7 @@ def basic_sms_numbers_list_request(): @pytest.fixture def sms_numbers_list_request_with_filters(): """SMS numbers list request with filters""" - return SmsNumbersListRequest( - paused=False, - page=1, - limit=10 - ) + return SmsNumbersListRequest(paused=False, page=1, limit=10) @pytest.fixture @@ -42,10 +38,7 @@ def sms_number_id_from_env(): @pytest.fixture def sms_number_update_request(sms_number_id_from_env): """SMS number update request""" - return SmsNumberUpdateRequest( - sms_number_id=sms_number_id_from_env, - paused=True - ) + return SmsNumberUpdateRequest(sms_number_id=sms_number_id_from_env, paused=True) @pytest.fixture @@ -84,7 +77,9 @@ def test_list_sms_numbers_basic(self, email_client, basic_sms_numbers_list_reque assert "created_at" in first_sms_number @vcr.use_cassette("sms_numbers_list_with_filters.yaml") - def test_list_sms_numbers_with_filters(self, email_client, sms_numbers_list_request_with_filters): + def test_list_sms_numbers_with_filters( + self, email_client, sms_numbers_list_request_with_filters + ): """Test listing SMS numbers with filters.""" response = email_client.sms_numbers.list(sms_numbers_list_request_with_filters) @@ -132,43 +127,54 @@ def test_list_sms_numbers_active_only(self, email_client): # ============================================================================ @vcr.use_cassette("sms_numbers_get_not_found.yaml") - def test_get_sms_number_not_found_with_test_id(self, email_client, sms_number_get_request): + def test_get_sms_number_not_found_with_test_id( + self, email_client, sms_number_get_request + ): """Test getting a non-existent SMS number returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.sms_numbers.get(sms_number_get_request) error_str = str(exc_info.value).lower() - assert ("not found" in error_str or "404" in error_str or - "could not be found" in error_str or - "sms" in error_str or "number" in error_str) + assert ( + "not found" in error_str + or "404" in error_str + or "could not be found" in error_str + or "sms" in error_str + or "number" in error_str + ) # ============================================================================ # SMS Number Update Tests # ============================================================================ @vcr.use_cassette("sms_numbers_update_not_found.yaml") - def test_update_sms_number_not_found_with_test_id(self, email_client, sms_number_update_request): + def test_update_sms_number_not_found_with_test_id( + self, email_client, sms_number_update_request + ): """Test updating a non-existent SMS number returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.sms_numbers.update(sms_number_update_request) error_str = str(exc_info.value).lower() - assert ("not found" in error_str or "404" in error_str or - "could not be found" in error_str or - "sms" in error_str or "number" in error_str) + assert ( + "not found" in error_str + or "404" in error_str + or "could not be found" in error_str + or "sms" in error_str + or "number" in error_str + ) @vcr.use_cassette("sms_numbers_update_pause.yaml") def test_update_sms_number_pause(self, email_client, sms_number_id_from_env): """Test pausing an SMS number.""" from mailersend.exceptions import ResourceNotFoundError - + request = SmsNumberUpdateRequest( - sms_number_id=sms_number_id_from_env, - paused=True + sms_number_id=sms_number_id_from_env, paused=True ) # This will likely fail with 404 for test SMS number ID @@ -176,18 +182,21 @@ def test_update_sms_number_pause(self, email_client, sms_number_id_from_env): email_client.sms_numbers.update(request) error_str = str(exc_info.value).lower() - assert ("not found" in error_str or "404" in error_str or - "could not be found" in error_str or - "sms" in error_str or "number" in error_str) + assert ( + "not found" in error_str + or "404" in error_str + or "could not be found" in error_str + or "sms" in error_str + or "number" in error_str + ) @vcr.use_cassette("sms_numbers_update_unpause.yaml") def test_update_sms_number_unpause(self, email_client, sms_number_id_from_env): """Test unpausing an SMS number.""" from mailersend.exceptions import ResourceNotFoundError - + request = SmsNumberUpdateRequest( - sms_number_id=sms_number_id_from_env, - paused=False + sms_number_id=sms_number_id_from_env, paused=False ) # This will likely fail with 404 for test SMS number ID @@ -195,26 +204,36 @@ def test_update_sms_number_unpause(self, email_client, sms_number_id_from_env): email_client.sms_numbers.update(request) error_str = str(exc_info.value).lower() - assert ("not found" in error_str or "404" in error_str or - "could not be found" in error_str or - "sms" in error_str or "number" in error_str) + assert ( + "not found" in error_str + or "404" in error_str + or "could not be found" in error_str + or "sms" in error_str + or "number" in error_str + ) # ============================================================================ # SMS Number Delete Tests # ============================================================================ @vcr.use_cassette("sms_numbers_delete_not_found.yaml") - def test_delete_sms_number_not_found_with_test_id(self, email_client, sms_number_delete_request): + def test_delete_sms_number_not_found_with_test_id( + self, email_client, sms_number_delete_request + ): """Test deleting a non-existent SMS number returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.sms_numbers.delete(sms_number_delete_request) error_str = str(exc_info.value).lower() - assert ("not found" in error_str or "404" in error_str or - "could not be found" in error_str or - "sms" in error_str or "number" in error_str) + assert ( + "not found" in error_str + or "404" in error_str + or "could not be found" in error_str + or "sms" in error_str + or "number" in error_str + ) # ============================================================================ # Validation and Error Handling Tests @@ -229,10 +248,7 @@ def test_list_sms_numbers_validation_error(self, email_client): # Should raise an AttributeError for invalid request type error_str = str(exc_info.value).lower() - assert ( - "attribute" in error_str - or "to_query_params" in error_str - ) + assert "attribute" in error_str or "to_query_params" in error_str @vcr.use_cassette("sms_numbers_api_response_structure.yaml") def test_api_response_structure(self, email_client, basic_sms_numbers_list_request): @@ -256,7 +272,9 @@ def test_api_response_structure(self, email_client, basic_sms_numbers_list_reque assert len(response.request_id) > 0 @vcr.use_cassette("sms_numbers_empty_result.yaml") - def test_list_sms_numbers_empty_result(self, email_client, basic_sms_numbers_list_request): + def test_list_sms_numbers_empty_result( + self, email_client, basic_sms_numbers_list_request + ): """Test listing SMS numbers when no SMS numbers exist.""" response = email_client.sms_numbers.list(basic_sms_numbers_list_request) @@ -290,16 +308,14 @@ def test_sms_number_update_model_validation(self): # Test valid update request with paused=True request = SmsNumberUpdateRequest( - sms_number_id="valid-sms-number-id", - paused=True + sms_number_id="valid-sms-number-id", paused=True ) assert request.sms_number_id == "valid-sms-number-id" assert request.paused is True # Test valid update request with paused=False request2 = SmsNumberUpdateRequest( - sms_number_id="valid-sms-number-id", - paused=False + sms_number_id="valid-sms-number-id", paused=False ) assert request2.paused is False @@ -330,11 +346,7 @@ def test_sms_numbers_list_query_params(self): assert params_paused["paused"] == "true" # Test with all filters - request_all = SmsNumbersListRequest( - paused=False, - page=2, - limit=25 - ) + request_all = SmsNumbersListRequest(paused=False, page=2, limit=25) params_all = request_all.to_query_params() assert params_all["paused"] == "false" assert params_all["page"] == 2 @@ -343,27 +355,21 @@ def test_sms_numbers_list_query_params(self): def test_sms_number_update_to_json(self): """Test SMS number update request JSON conversion.""" # Test with paused=True - request = SmsNumberUpdateRequest( - sms_number_id="test-id", - paused=True - ) - + request = SmsNumberUpdateRequest(sms_number_id="test-id", paused=True) + json_data = request.to_json() assert json_data["paused"] is True assert "sms_number_id" not in json_data # ID goes in URL, not body # Test with paused=False - request2 = SmsNumberUpdateRequest( - sms_number_id="test-id", - paused=False - ) - + request2 = SmsNumberUpdateRequest(sms_number_id="test-id", paused=False) + json_data2 = request2.to_json() assert json_data2["paused"] is False # Test without paused field request3 = SmsNumberUpdateRequest(sms_number_id="test-id") - + json_data3 = request3.to_json() assert json_data3 == {} # Empty payload when no fields to update @@ -423,10 +429,6 @@ def test_sms_numbers_list_parameter_combinations(self): assert params_both == {"page": 3, "limit": 20} # Test all parameters with None values - request_all_none = SmsNumbersListRequest( - paused=None, - page=None, - limit=None - ) + request_all_none = SmsNumbersListRequest(paused=None, page=None, limit=None) params_all_none = request_all_none.to_query_params() - assert params_all_none == {} \ No newline at end of file + assert params_all_none == {} diff --git a/tests/integration/test_sms_recipients.py b/tests/integration/test_sms_recipients.py index b151542..538a2bc 100644 --- a/tests/integration/test_sms_recipients.py +++ b/tests/integration/test_sms_recipients.py @@ -37,9 +37,13 @@ class TestSmsRecipientsIntegration: """Integration tests for SMS Recipients API.""" @vcr.use_cassette("sms_recipients_list_basic.yaml") - def test_list_sms_recipients_basic(self, email_client, basic_sms_recipients_list_request): + def test_list_sms_recipients_basic( + self, email_client, basic_sms_recipients_list_request + ): """Test listing SMS recipients with basic parameters.""" - response = email_client.sms_recipients.list_sms_recipients(basic_sms_recipients_list_request) + response = email_client.sms_recipients.list_sms_recipients( + basic_sms_recipients_list_request + ) assert isinstance(response, APIResponse) assert response.status_code == 200 @@ -83,9 +87,7 @@ def test_list_sms_recipients_with_status_filter(self, email_client): """Test listing SMS recipients with status filter.""" request = SmsRecipientsListRequest( query_params=SmsRecipientsListQueryParams( - status=SmsRecipientStatus.ACTIVE, - page=1, - limit=10 + status=SmsRecipientStatus.ACTIVE, page=1, limit=10 ) ) @@ -101,15 +103,15 @@ def test_list_sms_recipients_with_status_filter(self, email_client): assert recipient["status"] == "active" @vcr.use_cassette("sms_recipients_list_with_sms_number_filter.yaml") - def test_list_sms_recipients_with_sms_number_filter(self, email_client, test_sms_number_id): + def test_list_sms_recipients_with_sms_number_filter( + self, email_client, test_sms_number_id + ): """Test listing SMS recipients with SMS number ID filter.""" from mailersend.exceptions import BadRequestError - + request = SmsRecipientsListRequest( query_params=SmsRecipientsListQueryParams( - sms_number_id=test_sms_number_id, - page=1, - limit=10 + sms_number_id=test_sms_number_id, page=1, limit=10 ) ) @@ -126,26 +128,33 @@ def test_list_sms_recipients_with_sms_number_filter(self, email_client, test_sms def test_get_sms_recipient_not_found(self, email_client, sms_recipient_get_request): """Test getting a non-existent SMS recipient returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.sms_recipients.get_sms_recipient(sms_recipient_get_request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) or "could not be found" in str(exc_info.value).lower() + assert ( + "not found" in str(exc_info.value).lower() + or "404" in str(exc_info.value) + or "could not be found" in str(exc_info.value).lower() + ) @vcr.use_cassette("sms_recipients_update_not_found.yaml") def test_update_sms_recipient_not_found(self, email_client): """Test updating non-existent SMS recipient returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + request = SmsRecipientUpdateRequest( - sms_recipient_id="test-sms-recipient-id", - status=SmsRecipientStatus.OPT_OUT + sms_recipient_id="test-sms-recipient-id", status=SmsRecipientStatus.OPT_OUT ) with pytest.raises(ResourceNotFoundError) as exc_info: email_client.sms_recipients.update_sms_recipient(request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) or "could not be found" in str(exc_info.value).lower() + assert ( + "not found" in str(exc_info.value).lower() + or "404" in str(exc_info.value) + or "could not be found" in str(exc_info.value).lower() + ) @vcr.use_cassette("sms_recipients_validation_error.yaml") def test_list_sms_recipients_validation_error(self, email_client): @@ -156,15 +165,16 @@ def test_list_sms_recipients_validation_error(self, email_client): # Should raise an AttributeError for invalid request type error_str = str(exc_info.value).lower() - assert ( - "attribute" in error_str - or "to_query_params" in error_str - ) + assert "attribute" in error_str or "to_query_params" in error_str @vcr.use_cassette("sms_recipients_api_response_structure.yaml") - def test_api_response_structure(self, email_client, basic_sms_recipients_list_request): + def test_api_response_structure( + self, email_client, basic_sms_recipients_list_request + ): """Test that API response has the expected structure and metadata.""" - response = email_client.sms_recipients.list_sms_recipients(basic_sms_recipients_list_request) + response = email_client.sms_recipients.list_sms_recipients( + basic_sms_recipients_list_request + ) assert isinstance(response, APIResponse) assert response.status_code == 200 @@ -183,9 +193,13 @@ def test_api_response_structure(self, email_client, basic_sms_recipients_list_re assert len(response.request_id) > 0 @vcr.use_cassette("sms_recipients_empty_result.yaml") - def test_list_sms_recipients_empty_result(self, email_client, basic_sms_recipients_list_request): + def test_list_sms_recipients_empty_result( + self, email_client, basic_sms_recipients_list_request + ): """Test listing SMS recipients when no recipients exist.""" - response = email_client.sms_recipients.list_sms_recipients(basic_sms_recipients_list_request) + response = email_client.sms_recipients.list_sms_recipients( + basic_sms_recipients_list_request + ) assert isinstance(response, APIResponse) assert response.status_code == 200 @@ -204,9 +218,9 @@ def test_builder_list_basic_usage(self, email_client): """Test basic SMS recipients list using builder.""" builder = SmsRecipientsBuilder() request = builder.page(1).limit(10).build_list_request() - + response = email_client.sms_recipients.list_sms_recipients(request) - + assert isinstance(response, APIResponse) assert response.status_code == 200 @@ -214,29 +228,33 @@ def test_builder_list_basic_usage(self, email_client): def test_builder_list_with_status_filter(self, email_client): """Test SMS recipients list with status filter using builder.""" builder = SmsRecipientsBuilder() - request = (builder - .status(SmsRecipientStatus.ACTIVE) + request = ( + builder.status(SmsRecipientStatus.ACTIVE) .page(1) .limit(25) - .build_list_request()) - + .build_list_request() + ) + response = email_client.sms_recipients.list_sms_recipients(request) - + assert isinstance(response, APIResponse) assert response.status_code == 200 @vcr.use_cassette("sms_recipients_builder_list_with_sms_number.yaml") - def test_builder_list_with_sms_number_filter(self, email_client, test_sms_number_id): + def test_builder_list_with_sms_number_filter( + self, email_client, test_sms_number_id + ): """Test SMS recipients list with SMS number filter using builder.""" from mailersend.exceptions import BadRequestError - + builder = SmsRecipientsBuilder() - request = (builder - .sms_number_id(test_sms_number_id) + request = ( + builder.sms_number_id(test_sms_number_id) .page(1) .limit(10) - .build_list_request()) - + .build_list_request() + ) + try: response = email_client.sms_recipients.list_sms_recipients(request) assert isinstance(response, APIResponse) @@ -249,10 +267,10 @@ def test_builder_list_with_sms_number_filter(self, email_client, test_sms_number def test_builder_get_not_found(self, email_client): """Test getting non-existent SMS recipient using builder.""" from mailersend.exceptions import ResourceNotFoundError - + builder = SmsRecipientsBuilder() request = builder.sms_recipient_id("test-sms-recipient-id").build_get_request() - + with pytest.raises(ResourceNotFoundError): email_client.sms_recipients.get_sms_recipient(request) @@ -260,39 +278,40 @@ def test_builder_get_not_found(self, email_client): def test_builder_update_not_found(self, email_client): """Test updating non-existent SMS recipient using builder.""" from mailersend.exceptions import ResourceNotFoundError - + builder = SmsRecipientsBuilder() - request = builder.sms_recipient_id("test-sms-recipient-id").build_update_request( - status=SmsRecipientStatus.OPT_OUT - ) - + request = builder.sms_recipient_id( + "test-sms-recipient-id" + ).build_update_request(status=SmsRecipientStatus.OPT_OUT) + with pytest.raises(ResourceNotFoundError): email_client.sms_recipients.update_sms_recipient(request) def test_builder_fluent_interface(self): """Test that builder methods return self for chaining.""" builder = SmsRecipientsBuilder() - + # Test method chaining - result = (builder - .page(1) + result = ( + builder.page(1) .limit(10) .status(SmsRecipientStatus.ACTIVE) .sms_number_id("test-sms-number") - .sms_recipient_id("test-recipient")) - + .sms_recipient_id("test-recipient") + ) + assert result is builder - + # Verify the builder state for different requests list_request = builder.build_list_request() assert list_request.query_params.page == 1 assert list_request.query_params.limit == 10 assert list_request.query_params.status == SmsRecipientStatus.ACTIVE assert list_request.query_params.sms_number_id == "test-sms-number" - + get_request = builder.build_get_request() assert get_request.sms_recipient_id == "test-recipient" - + update_request = builder.build_update_request(SmsRecipientStatus.OPT_OUT) assert update_request.sms_recipient_id == "test-recipient" assert update_request.status == SmsRecipientStatus.OPT_OUT @@ -300,57 +319,64 @@ def test_builder_fluent_interface(self): def test_builder_validation_errors(self): """Test builder validation for missing required fields.""" builder = SmsRecipientsBuilder() - + # Test building get request without SMS recipient ID with pytest.raises(ValueError) as exc_info: builder.build_get_request() assert "sms recipient id is required" in str(exc_info.value).lower() - + # Test building update request without SMS recipient ID with pytest.raises(ValueError) as exc_info: builder.build_update_request(SmsRecipientStatus.ACTIVE) assert "sms recipient id is required" in str(exc_info.value).lower() @vcr.use_cassette("sms_recipients_comprehensive_workflow.yaml") - def test_comprehensive_sms_recipients_workflow(self, email_client, test_sms_number_id): + def test_comprehensive_sms_recipients_workflow( + self, email_client, test_sms_number_id + ): """Test comprehensive workflow covering list operations, error scenarios, and builder usage.""" # Test list with different configurations list_request = SmsRecipientsListRequest( query_params=SmsRecipientsListQueryParams(page=1, limit=10) ) - + response = email_client.sms_recipients.list_sms_recipients(list_request) assert isinstance(response, APIResponse) assert response.status_code == 200 - + # Test builder pattern with different configurations builder = SmsRecipientsBuilder() - + # Test list with builder and filters from mailersend.exceptions import BadRequestError - - builder_request = (builder - .status(SmsRecipientStatus.ACTIVE) + + builder_request = ( + builder.status(SmsRecipientStatus.ACTIVE) .page(1) .limit(25) - .build_list_request()) + .build_list_request() + ) try: - builder_response = email_client.sms_recipients.list_sms_recipients(builder_request) + builder_response = email_client.sms_recipients.list_sms_recipients( + builder_request + ) assert isinstance(builder_response, APIResponse) assert builder_response.status_code == 200 except BadRequestError: # Handle potential API errors in test environment pass - + # Test error scenarios from mailersend.exceptions import ResourceNotFoundError - + # Test get non-existent SMS recipient get_request = SmsRecipientGetRequest(sms_recipient_id="non-existent-id") with pytest.raises(ResourceNotFoundError): email_client.sms_recipients.get_sms_recipient(get_request) - + # Test builder error scenarios - builder_get_request = builder.sms_recipient_id("another-non-existent-id").build_get_request() + builder_get_request = builder.sms_recipient_id( + "another-non-existent-id" + ).build_get_request() with pytest.raises(ResourceNotFoundError): - email_client.sms_recipients.get_sms_recipient(builder_get_request) \ No newline at end of file + email_client.sms_recipients.get_sms_recipient(builder_get_request) diff --git a/tests/integration/test_sms_sending.py b/tests/integration/test_sms_sending.py index d3317b5..43ace4f 100644 --- a/tests/integration/test_sms_sending.py +++ b/tests/integration/test_sms_sending.py @@ -21,7 +21,7 @@ def basic_sms_send_request(sms_phone_number): return SmsSendRequest( from_number=sms_phone_number, to=[sms_phone_number], # Use same number for from/to in test environment - text="Hello, this is a test SMS message!" + text="Hello, this is a test SMS message!", ) @@ -31,17 +31,14 @@ def sms_send_request_with_personalization(sms_phone_number): # For personalization test, we'll use the same number but simulate multiple recipients # In a real scenario, you'd have different numbers recipient_number = sms_phone_number - + return SmsSendRequest( from_number=sms_phone_number, to=[recipient_number], # Single recipient for test environment text="Hello {{name}}, this is a personalized message!", personalization=[ - SmsPersonalization( - phone_number=recipient_number, - data={"name": "John"} - ) - ] + SmsPersonalization(phone_number=recipient_number, data={"name": "John"}) + ], ) @@ -51,7 +48,7 @@ def sample_sms_data(sms_phone_number): return { "from_number": sms_phone_number, "to": [sms_phone_number], - "text": "Test message for validation" + "text": "Test message for validation", } @@ -66,71 +63,93 @@ class TestSmsSendingIntegration: def test_send_sms_basic(self, email_client, basic_sms_send_request): """Test sending a basic SMS message.""" from mailersend.exceptions import BadRequestError, ResourceNotFoundError - + # This will likely fail due to invalid phone numbers or missing SMS configuration # but we want to test that the API call structure is correct with pytest.raises((BadRequestError, ResourceNotFoundError)) as exc_info: response = email_client.sms_sending.send(basic_sms_send_request) error_str = str(exc_info.value).lower() - assert ("phone" in error_str or "number" in error_str or - "sms" in error_str or "not found" in error_str or - "invalid" in error_str or "from" in error_str) + assert ( + "phone" in error_str + or "number" in error_str + or "sms" in error_str + or "not found" in error_str + or "invalid" in error_str + or "from" in error_str + ) @vcr.use_cassette("sms_send_with_personalization.yaml") - def test_send_sms_with_personalization(self, email_client, sms_send_request_with_personalization): + def test_send_sms_with_personalization( + self, email_client, sms_send_request_with_personalization + ): """Test sending SMS with personalization data.""" from mailersend.exceptions import BadRequestError, ResourceNotFoundError - + # This will likely fail due to invalid phone numbers or missing SMS configuration with pytest.raises((BadRequestError, ResourceNotFoundError)) as exc_info: - response = email_client.sms_sending.send(sms_send_request_with_personalization) + response = email_client.sms_sending.send( + sms_send_request_with_personalization + ) error_str = str(exc_info.value).lower() - assert ("phone" in error_str or "number" in error_str or - "sms" in error_str or "not found" in error_str or - "invalid" in error_str or "from" in error_str) + assert ( + "phone" in error_str + or "number" in error_str + or "sms" in error_str + or "not found" in error_str + or "invalid" in error_str + or "from" in error_str + ) @vcr.use_cassette("sms_send_long_message.yaml") def test_send_sms_long_message(self, email_client, sms_phone_number): """Test sending SMS with a long message.""" from mailersend.exceptions import BadRequestError, ResourceNotFoundError - + # Create a request with a long message (but under 2048 limit) long_text = "This is a long SMS message. " * 50 # ~1400 characters request = SmsSendRequest( - from_number=sms_phone_number, - to=[sms_phone_number], - text=long_text + from_number=sms_phone_number, to=[sms_phone_number], text=long_text ) with pytest.raises((BadRequestError, ResourceNotFoundError)) as exc_info: response = email_client.sms_sending.send(request) error_str = str(exc_info.value).lower() - assert ("phone" in error_str or "number" in error_str or - "sms" in error_str or "not found" in error_str or - "invalid" in error_str or "from" in error_str) + assert ( + "phone" in error_str + or "number" in error_str + or "sms" in error_str + or "not found" in error_str + or "invalid" in error_str + or "from" in error_str + ) @vcr.use_cassette("sms_send_multiple_recipients.yaml") def test_send_sms_multiple_recipients(self, email_client, sms_phone_number): """Test sending SMS to multiple recipients.""" from mailersend.exceptions import BadRequestError, ResourceNotFoundError - + # In test environment, use same number multiple times to simulate multiple recipients request = SmsSendRequest( from_number=sms_phone_number, to=[sms_phone_number], # Single recipient in test environment - text="Message for multiple recipients" + text="Message for multiple recipients", ) with pytest.raises((BadRequestError, ResourceNotFoundError)) as exc_info: response = email_client.sms_sending.send(request) error_str = str(exc_info.value).lower() - assert ("phone" in error_str or "number" in error_str or - "sms" in error_str or "not found" in error_str or - "invalid" in error_str or "from" in error_str) + assert ( + "phone" in error_str + or "number" in error_str + or "sms" in error_str + or "not found" in error_str + or "invalid" in error_str + or "from" in error_str + ) # ============================================================================ # Validation and Error Handling Tests @@ -145,13 +164,10 @@ def test_send_sms_validation_error(self, email_client): # Should raise an AttributeError for invalid request type error_str = str(exc_info.value).lower() - assert ( - "attribute" in error_str - or "to_json" in error_str - ) + assert "attribute" in error_str or "to_json" in error_str # ============================================================================ - # Model Validation Tests + # Model Validation Tests # ============================================================================ def test_sms_send_model_validation_phone_format(self): @@ -161,7 +177,7 @@ def test_sms_send_model_validation_phone_format(self): SmsSendRequest( from_number="1234567890", # Missing + to=["+1234567891"], - text="Test message" + text="Test message", ) assert "e164 format" in str(exc_info.value).lower() @@ -170,7 +186,7 @@ def test_sms_send_model_validation_phone_format(self): SmsSendRequest( from_number="+1234567890", to=["1234567891"], # Missing + - text="Test message" + text="Test message", ) assert "e164 format" in str(exc_info.value).lower() @@ -178,20 +194,12 @@ def test_sms_send_model_validation_text_length(self): """Test model validation for text message length.""" # Test empty text with pytest.raises(ValueError) as exc_info: - SmsSendRequest( - from_number="+1234567890", - to=["+1234567891"], - text="" - ) + SmsSendRequest(from_number="+1234567890", to=["+1234567891"], text="") assert "cannot be empty" in str(exc_info.value).lower() # Test whitespace-only text with pytest.raises(ValueError) as exc_info: - SmsSendRequest( - from_number="+1234567890", - to=["+1234567891"], - text=" " - ) + SmsSendRequest(from_number="+1234567890", to=["+1234567891"], text=" ") assert "cannot be empty" in str(exc_info.value).lower() # Test text too long @@ -199,7 +207,7 @@ def test_sms_send_model_validation_text_length(self): SmsSendRequest( from_number="+1234567890", to=["+1234567891"], - text="x" * 2049 # Over 2048 limit + text="x" * 2049, # Over 2048 limit ) assert "2048 characters" in str(exc_info.value).lower() @@ -208,9 +216,7 @@ def test_sms_send_model_validation_recipients(self): # Test empty recipients list with pytest.raises(ValueError): SmsSendRequest( - from_number="+1234567890", - to=[], # Empty list - text="Test message" + from_number="+1234567890", to=[], text="Test message" # Empty list ) # Test too many recipients (over 50) @@ -218,7 +224,7 @@ def test_sms_send_model_validation_recipients(self): SmsSendRequest( from_number="+1234567890", to=[f"+123456789{i:02d}" for i in range(51)], # 51 recipients - text="Test message" + text="Test message", ) def test_sms_personalization_validation(self): @@ -226,8 +232,7 @@ def test_sms_personalization_validation(self): # Test invalid phone number in personalization (no +) with pytest.raises(ValueError) as exc_info: SmsPersonalization( - phone_number="1234567890", # Missing + - data={"name": "John"} + phone_number="1234567890", data={"name": "John"} # Missing + ) assert "e164 format" in str(exc_info.value).lower() @@ -241,36 +246,30 @@ def test_sms_send_personalization_mismatch(self): personalization=[ SmsPersonalization( phone_number="+1234567892", # Not in 'to' list - data={"name": "John"} + data={"name": "John"}, ) - ] + ], ) assert "not in recipient list" in str(exc_info.value).lower() def test_sms_send_to_json(self, sms_phone_number): """Test SmsSendRequest JSON conversion.""" # Use different test numbers for this validation test - recipient1 = "+1234567891" + recipient1 = "+1234567891" recipient2 = "+1234567892" - + request = SmsSendRequest( from_number=sms_phone_number, to=[recipient1, recipient2], text="Hello {{name}}!", personalization=[ - SmsPersonalization( - phone_number=recipient1, - data={"name": "John"} - ), - SmsPersonalization( - phone_number=recipient2, - data={"name": "Jane"} - ) - ] + SmsPersonalization(phone_number=recipient1, data={"name": "John"}), + SmsPersonalization(phone_number=recipient2, data={"name": "Jane"}), + ], ) - + json_data = request.to_json() - + assert json_data["from"] == sms_phone_number assert json_data["to"] == [recipient1, recipient2] assert json_data["text"] == "Hello {{name}}!" @@ -284,15 +283,13 @@ def test_sms_send_to_json(self, sms_phone_number): def test_sms_send_to_json_no_personalization(self, sms_phone_number): """Test SmsSendRequest JSON conversion without personalization.""" recipient = "+1234567891" - + request = SmsSendRequest( - from_number=sms_phone_number, - to=[recipient], - text="Simple message" + from_number=sms_phone_number, to=[recipient], text="Simple message" ) - + json_data = request.to_json() - + assert json_data["from"] == sms_phone_number assert json_data["to"] == [recipient] assert json_data["text"] == "Simple message" @@ -307,10 +304,10 @@ def test_sms_personalization_data_types(self): "age": 25, "is_premium": True, "balance": 99.99, - "tags": ["customer", "vip"] - } + "tags": ["customer", "vip"], + }, ) - + assert personalization.phone_number == "+1234567890" assert personalization.data["name"] == "John" assert personalization.data["age"] == 25 @@ -321,20 +318,11 @@ def test_sms_personalization_data_types(self): def test_phone_number_edge_cases(self): """Test phone number validation edge cases.""" # Valid E164 numbers - valid_numbers = [ - "+1234567890", - "+44123456789", - "+33123456789", - "+49123456789" - ] - + valid_numbers = ["+1234567890", "+44123456789", "+33123456789", "+49123456789"] + for number in valid_numbers: # Should not raise exception - request = SmsSendRequest( - from_number=number, - to=[number], - text="Test" - ) + request = SmsSendRequest(from_number=number, to=[number], text="Test") assert request.from_number == number assert request.to == [number] @@ -343,18 +331,14 @@ def test_text_message_edge_cases(self): # Test exact limit (2048 characters) max_text = "x" * 2048 request = SmsSendRequest( - from_number="+1234567890", - to=["+1234567891"], - text=max_text + from_number="+1234567890", to=["+1234567891"], text=max_text ) assert len(request.text) == 2048 # Test text with special characters and emojis special_text = "Hello! 🚀 Special chars: àáâãäå æç èéêë ìíîï ñòóôõö ùúûü ý" request2 = SmsSendRequest( - from_number="+1234567890", - to=["+1234567891"], - text=special_text + from_number="+1234567890", to=["+1234567891"], text=special_text ) assert request2.text == special_text @@ -363,16 +347,12 @@ def test_recipients_limit_validation(self): # Test exactly 50 recipients (should work) fifty_recipients = [f"+123456789{i:02d}" for i in range(50)] request = SmsSendRequest( - from_number="+1234567890", - to=fifty_recipients, - text="Bulk message" + from_number="+1234567890", to=fifty_recipients, text="Bulk message" ) assert len(request.to) == 50 # Test single recipient request2 = SmsSendRequest( - from_number="+1234567890", - to=["+1234567891"], - text="Single message" + from_number="+1234567890", to=["+1234567891"], text="Single message" ) - assert len(request2.to) == 1 \ No newline at end of file + assert len(request2.to) == 1 diff --git a/tests/integration/test_sms_webhooks.py b/tests/integration/test_sms_webhooks.py index 99ae4ab..e9ae08a 100644 --- a/tests/integration/test_sms_webhooks.py +++ b/tests/integration/test_sms_webhooks.py @@ -42,17 +42,15 @@ def sample_sms_webhook_create_request(sms_number_id_from_env): name="Test SMS Webhook", events=[SmsWebhookEvent.SMS_SENT, SmsWebhookEvent.SMS_DELIVERED], enabled=True, - sms_number_id=sms_number_id_from_env + sms_number_id=sms_number_id_from_env, ) -@pytest.fixture +@pytest.fixture def sms_webhook_update_request(): """SMS webhook update request""" return SmsWebhookUpdateRequest( - sms_webhook_id="test-sms-webhook-id", - name="Updated SMS Webhook", - enabled=False + sms_webhook_id="test-sms-webhook-id", name="Updated SMS Webhook", enabled=False ) @@ -70,146 +68,196 @@ class TestSmsWebhooksIntegration: # ============================================================================ @vcr.use_cassette("sms_webhooks_list_basic.yaml") - def test_list_sms_webhooks_basic(self, email_client, basic_sms_webhooks_list_request): + def test_list_sms_webhooks_basic( + self, email_client, basic_sms_webhooks_list_request + ): """Test listing SMS webhooks with basic parameters.""" from mailersend.exceptions import BadRequestError, ResourceNotFoundError - + # This will likely fail due to invalid SMS number ID or missing webhook configuration with pytest.raises((BadRequestError, ResourceNotFoundError)) as exc_info: - response = email_client.sms_webhooks.list_sms_webhooks(basic_sms_webhooks_list_request) + response = email_client.sms_webhooks.list_sms_webhooks( + basic_sms_webhooks_list_request + ) error_str = str(exc_info.value).lower() - assert ("sms" in error_str or "number" in error_str or - "webhook" in error_str or "not found" in error_str or - "invalid" in error_str or "id" in error_str) + assert ( + "sms" in error_str + or "number" in error_str + or "webhook" in error_str + or "not found" in error_str + or "invalid" in error_str + or "id" in error_str + ) @vcr.use_cassette("sms_webhooks_list_with_invalid_sms_number.yaml") def test_list_sms_webhooks_with_invalid_sms_number(self, email_client): """Test listing SMS webhooks with invalid SMS number ID.""" from mailersend.exceptions import BadRequestError, ResourceNotFoundError - + request = SmsWebhooksListRequest( - query_params=SmsWebhooksListQueryParams(sms_number_id="invalid-sms-number-id") + query_params=SmsWebhooksListQueryParams( + sms_number_id="invalid-sms-number-id" + ) ) with pytest.raises((BadRequestError, ResourceNotFoundError)) as exc_info: email_client.sms_webhooks.list_sms_webhooks(request) error_str = str(exc_info.value).lower() - assert ("sms" in error_str or "number" in error_str or - "not found" in error_str or "invalid" in error_str) + assert ( + "sms" in error_str + or "number" in error_str + or "not found" in error_str + or "invalid" in error_str + ) # ============================================================================ # SMS Webhook Get Tests # ============================================================================ @vcr.use_cassette("sms_webhooks_get_not_found.yaml") - def test_get_sms_webhook_not_found_with_test_id(self, email_client, sms_webhook_get_request): + def test_get_sms_webhook_not_found_with_test_id( + self, email_client, sms_webhook_get_request + ): """Test getting a non-existent SMS webhook returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.sms_webhooks.get_sms_webhook(sms_webhook_get_request) error_str = str(exc_info.value).lower() - assert ("not found" in error_str or "404" in error_str or - "could not be found" in error_str or - "webhook" in error_str or "sms" in error_str) + assert ( + "not found" in error_str + or "404" in error_str + or "could not be found" in error_str + or "webhook" in error_str + or "sms" in error_str + ) # ============================================================================ # SMS Webhook Create Tests # ============================================================================ @vcr.use_cassette("sms_webhooks_create_invalid_sms_number.yaml") - def test_create_sms_webhook_invalid_sms_number(self, email_client, sample_sms_webhook_create_request): + def test_create_sms_webhook_invalid_sms_number( + self, email_client, sample_sms_webhook_create_request + ): """Test creating SMS webhook with invalid SMS number ID.""" from mailersend.exceptions import BadRequestError, ResourceNotFoundError - + # This will likely fail due to invalid SMS number ID with pytest.raises((BadRequestError, ResourceNotFoundError)) as exc_info: - email_client.sms_webhooks.create_sms_webhook(sample_sms_webhook_create_request) + email_client.sms_webhooks.create_sms_webhook( + sample_sms_webhook_create_request + ) error_str = str(exc_info.value).lower() - assert ("sms" in error_str or "number" in error_str or - "webhook" in error_str or "not found" in error_str or - "invalid" in error_str or "id" in error_str) + assert ( + "sms" in error_str + or "number" in error_str + or "webhook" in error_str + or "not found" in error_str + or "invalid" in error_str + or "id" in error_str + ) @vcr.use_cassette("sms_webhooks_create_with_all_events.yaml") - def test_create_sms_webhook_with_all_events(self, email_client, sms_number_id_from_env): + def test_create_sms_webhook_with_all_events( + self, email_client, sms_number_id_from_env + ): """Test creating SMS webhook with all available events.""" from mailersend.exceptions import BadRequestError, ResourceNotFoundError - + request = SmsWebhookCreateRequest( url="https://example.com/all-events", name="All Events Webhook", events=[ SmsWebhookEvent.SMS_SENT, SmsWebhookEvent.SMS_DELIVERED, - SmsWebhookEvent.SMS_FAILED + SmsWebhookEvent.SMS_FAILED, ], enabled=True, - sms_number_id=sms_number_id_from_env + sms_number_id=sms_number_id_from_env, ) with pytest.raises((BadRequestError, ResourceNotFoundError)) as exc_info: email_client.sms_webhooks.create_sms_webhook(request) error_str = str(exc_info.value).lower() - assert ("sms" in error_str or "number" in error_str or - "webhook" in error_str or "not found" in error_str or - "invalid" in error_str) + assert ( + "sms" in error_str + or "number" in error_str + or "webhook" in error_str + or "not found" in error_str + or "invalid" in error_str + ) # ============================================================================ # SMS Webhook Update Tests # ============================================================================ @vcr.use_cassette("sms_webhooks_update_not_found.yaml") - def test_update_sms_webhook_not_found_with_test_id(self, email_client, sms_webhook_update_request): + def test_update_sms_webhook_not_found_with_test_id( + self, email_client, sms_webhook_update_request + ): """Test updating a non-existent SMS webhook returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.sms_webhooks.update_sms_webhook(sms_webhook_update_request) error_str = str(exc_info.value).lower() - assert ("not found" in error_str or "404" in error_str or - "could not be found" in error_str or - "webhook" in error_str or "sms" in error_str) + assert ( + "not found" in error_str + or "404" in error_str + or "could not be found" in error_str + or "webhook" in error_str + or "sms" in error_str + ) @vcr.use_cassette("sms_webhooks_update_disable.yaml") def test_update_sms_webhook_disable(self, email_client): """Test disabling an SMS webhook.""" from mailersend.exceptions import ResourceNotFoundError - + request = SmsWebhookUpdateRequest( - sms_webhook_id="test-sms-webhook-id", - enabled=False + sms_webhook_id="test-sms-webhook-id", enabled=False ) with pytest.raises(ResourceNotFoundError) as exc_info: email_client.sms_webhooks.update_sms_webhook(request) error_str = str(exc_info.value).lower() - assert ("not found" in error_str or "404" in error_str or - "could not be found" in error_str or - "webhook" in error_str) + assert ( + "not found" in error_str + or "404" in error_str + or "could not be found" in error_str + or "webhook" in error_str + ) # ============================================================================ # SMS Webhook Delete Tests # ============================================================================ @vcr.use_cassette("sms_webhooks_delete_not_found.yaml") - def test_delete_sms_webhook_not_found_with_test_id(self, email_client, sms_webhook_delete_request): + def test_delete_sms_webhook_not_found_with_test_id( + self, email_client, sms_webhook_delete_request + ): """Test deleting a non-existent SMS webhook returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.sms_webhooks.delete_sms_webhook(sms_webhook_delete_request) error_str = str(exc_info.value).lower() - assert ("not found" in error_str or "404" in error_str or - "could not be found" in error_str or - "webhook" in error_str or "sms" in error_str) + assert ( + "not found" in error_str + or "404" in error_str + or "could not be found" in error_str + or "webhook" in error_str + or "sms" in error_str + ) # ============================================================================ # Validation and Error Handling Tests @@ -224,7 +272,4 @@ def test_list_sms_webhooks_validation_error(self, email_client): # Should raise an AttributeError for invalid request type error_str = str(exc_info.value).lower() - assert ( - "attribute" in error_str - or "to_query_params" in error_str - ) \ No newline at end of file + assert "attribute" in error_str or "to_query_params" in error_str diff --git a/tests/integration/test_smtp_users.py b/tests/integration/test_smtp_users.py index 941f8ed..5fadb92 100644 --- a/tests/integration/test_smtp_users.py +++ b/tests/integration/test_smtp_users.py @@ -24,8 +24,7 @@ def test_domain_id(): def basic_smtp_users_list_request(test_domain_id): """Basic SMTP users list request""" return SmtpUsersListRequest( - domain_id=test_domain_id, - query_params=SmtpUsersListQueryParams(limit=10) + domain_id=test_domain_id, query_params=SmtpUsersListQueryParams(limit=10) ) @@ -33,19 +32,14 @@ def basic_smtp_users_list_request(test_domain_id): def smtp_user_get_request(test_domain_id): """SMTP user get request with test SMTP user ID""" return SmtpUserGetRequest( - domain_id=test_domain_id, - smtp_user_id="test-smtp-user-id" + domain_id=test_domain_id, smtp_user_id="test-smtp-user-id" ) @pytest.fixture def sample_smtp_user_data(test_domain_id): """Sample SMTP user data for testing""" - return { - "domain_id": test_domain_id, - "name": "Test SMTP User", - "enabled": True - } + return {"domain_id": test_domain_id, "name": "Test SMTP User", "enabled": True} class TestSmtpUsersIntegration: @@ -54,7 +48,9 @@ class TestSmtpUsersIntegration: @vcr.use_cassette("smtp_users_list_basic.yaml") def test_list_smtp_users_basic(self, email_client, basic_smtp_users_list_request): """Test listing SMTP users with basic parameters.""" - response = email_client.smtp_users.list_smtp_users(basic_smtp_users_list_request) + response = email_client.smtp_users.list_smtp_users( + basic_smtp_users_list_request + ) assert isinstance(response, APIResponse) assert response.status_code == 200 @@ -77,8 +73,7 @@ def test_list_smtp_users_basic(self, email_client, basic_smtp_users_list_request def test_list_smtp_users_with_limit(self, email_client, test_domain_id): """Test listing SMTP users with custom limit.""" request = SmtpUsersListRequest( - domain_id=test_domain_id, - query_params=SmtpUsersListQueryParams(limit=25) + domain_id=test_domain_id, query_params=SmtpUsersListQueryParams(limit=25) ) response = email_client.smtp_users.list_smtp_users(request) @@ -97,74 +92,94 @@ def test_list_smtp_users_with_limit(self, email_client, test_domain_id): def test_list_smtp_users_invalid_domain(self, email_client): """Test listing SMTP users with invalid domain ID returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + request = SmtpUsersListRequest( domain_id="invalid-domain-id", - query_params=SmtpUsersListQueryParams(limit=10) + query_params=SmtpUsersListQueryParams(limit=10), ) with pytest.raises(ResourceNotFoundError) as exc_info: email_client.smtp_users.list_smtp_users(request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) or "could not be found" in str(exc_info.value).lower() + assert ( + "not found" in str(exc_info.value).lower() + or "404" in str(exc_info.value) + or "could not be found" in str(exc_info.value).lower() + ) @vcr.use_cassette("smtp_users_get_not_found.yaml") def test_get_smtp_user_not_found(self, email_client, smtp_user_get_request): """Test getting a non-existent SMTP user returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.smtp_users.get_smtp_user(smtp_user_get_request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) or "could not be found" in str(exc_info.value).lower() + assert ( + "not found" in str(exc_info.value).lower() + or "404" in str(exc_info.value) + or "could not be found" in str(exc_info.value).lower() + ) @vcr.use_cassette("smtp_users_create_invalid_domain.yaml") def test_create_smtp_user_invalid_domain(self, email_client, sample_smtp_user_data): """Test creating SMTP user with invalid domain ID returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + request = SmtpUserCreateRequest( domain_id="invalid-domain-id", name=sample_smtp_user_data["name"], - enabled=sample_smtp_user_data["enabled"] + enabled=sample_smtp_user_data["enabled"], ) with pytest.raises(ResourceNotFoundError) as exc_info: email_client.smtp_users.create_smtp_user(request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) or "could not be found" in str(exc_info.value).lower() + assert ( + "not found" in str(exc_info.value).lower() + or "404" in str(exc_info.value) + or "could not be found" in str(exc_info.value).lower() + ) @vcr.use_cassette("smtp_users_update_not_found.yaml") def test_update_smtp_user_not_found(self, email_client, test_domain_id): """Test updating non-existent SMTP user returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + request = SmtpUserUpdateRequest( domain_id=test_domain_id, smtp_user_id="test-smtp-user-id", name="Updated SMTP User", - enabled=False + enabled=False, ) with pytest.raises(ResourceNotFoundError) as exc_info: email_client.smtp_users.update_smtp_user(request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) or "could not be found" in str(exc_info.value).lower() + assert ( + "not found" in str(exc_info.value).lower() + or "404" in str(exc_info.value) + or "could not be found" in str(exc_info.value).lower() + ) @vcr.use_cassette("smtp_users_delete_not_found.yaml") def test_delete_smtp_user_not_found(self, email_client, smtp_user_get_request): """Test deleting non-existent SMTP user returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + delete_request = SmtpUserDeleteRequest( domain_id=smtp_user_get_request.domain_id, - smtp_user_id=smtp_user_get_request.smtp_user_id + smtp_user_id=smtp_user_get_request.smtp_user_id, ) with pytest.raises(ResourceNotFoundError) as exc_info: email_client.smtp_users.delete_smtp_user(delete_request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) or "could not be found" in str(exc_info.value).lower() + assert ( + "not found" in str(exc_info.value).lower() + or "404" in str(exc_info.value) + or "could not be found" in str(exc_info.value).lower() + ) @vcr.use_cassette("smtp_users_validation_error.yaml") def test_list_smtp_users_validation_error(self, email_client): @@ -175,15 +190,14 @@ def test_list_smtp_users_validation_error(self, email_client): # Should raise an AttributeError for invalid request type error_str = str(exc_info.value).lower() - assert ( - "attribute" in error_str - or "to_query_params" in error_str - ) + assert "attribute" in error_str or "to_query_params" in error_str @vcr.use_cassette("smtp_users_api_response_structure.yaml") def test_api_response_structure(self, email_client, basic_smtp_users_list_request): """Test that API response has the expected structure and metadata.""" - response = email_client.smtp_users.list_smtp_users(basic_smtp_users_list_request) + response = email_client.smtp_users.list_smtp_users( + basic_smtp_users_list_request + ) assert isinstance(response, APIResponse) assert response.status_code == 200 @@ -202,9 +216,13 @@ def test_api_response_structure(self, email_client, basic_smtp_users_list_reques assert len(response.request_id) > 0 @vcr.use_cassette("smtp_users_empty_result.yaml") - def test_list_smtp_users_empty_result(self, email_client, basic_smtp_users_list_request): + def test_list_smtp_users_empty_result( + self, email_client, basic_smtp_users_list_request + ): """Test listing SMTP users when no users exist.""" - response = email_client.smtp_users.list_smtp_users(basic_smtp_users_list_request) + response = email_client.smtp_users.list_smtp_users( + basic_smtp_users_list_request + ) assert isinstance(response, APIResponse) assert response.status_code == 200 @@ -218,26 +236,20 @@ def test_smtp_user_create_model_validation(self): """Test model validation for SMTP user creation.""" # Test empty domain_id - this will raise a pydantic ValidationError first from pydantic import ValidationError + with pytest.raises(ValidationError) as exc_info: - SmtpUserCreateRequest( - domain_id="", - name="Test User" - ) + SmtpUserCreateRequest(domain_id="", name="Test User") assert "string should have at least 1 character" in str(exc_info.value).lower() # Test empty name - this will also raise a pydantic ValidationError first with pytest.raises(ValidationError) as exc_info: - SmtpUserCreateRequest( - domain_id="test-domain", - name="" - ) + SmtpUserCreateRequest(domain_id="test-domain", name="") assert "string should have at least 1 character" in str(exc_info.value).lower() # Test name too long - this will also raise a pydantic ValidationError first with pytest.raises(ValidationError) as exc_info: SmtpUserCreateRequest( - domain_id="test-domain", - name="x" * 51 # Exceeds 50 character limit + domain_id="test-domain", name="x" * 51 # Exceeds 50 character limit ) assert "string should have at most 50 characters" in str(exc_info.value).lower() @@ -245,19 +257,14 @@ def test_smtp_user_get_model_validation(self): """Test model validation for SMTP user retrieval.""" # Test empty domain_id - this will raise a pydantic ValidationError first from pydantic import ValidationError + with pytest.raises(ValidationError) as exc_info: - SmtpUserGetRequest( - domain_id="", - smtp_user_id="test-id" - ) + SmtpUserGetRequest(domain_id="", smtp_user_id="test-id") assert "string should have at least 1 character" in str(exc_info.value).lower() # Test empty smtp_user_id - this will also raise a pydantic ValidationError first with pytest.raises(ValidationError) as exc_info: - SmtpUserGetRequest( - domain_id="test-domain", - smtp_user_id="" - ) + SmtpUserGetRequest(domain_id="test-domain", smtp_user_id="") assert "string should have at least 1 character" in str(exc_info.value).lower() def test_smtp_users_list_query_params_validation(self): @@ -265,12 +272,12 @@ def test_smtp_users_list_query_params_validation(self): # Test valid parameters params = SmtpUsersListQueryParams(limit=25) assert params.limit == 25 - + # Test minimum limit validation with pytest.raises(ValueError): SmtpUsersListQueryParams(limit=5) # Below minimum of 10 - - # Test maximum limit validation + + # Test maximum limit validation with pytest.raises(ValueError): SmtpUsersListQueryParams(limit=150) # Above maximum of 100 @@ -283,9 +290,9 @@ def test_builder_list_basic_usage(self, email_client, test_domain_id): """Test basic SMTP users list using builder.""" builder = SmtpUsersBuilder() request = builder.domain_id(test_domain_id).limit(10).build_smtp_users_list() - + response = email_client.smtp_users.list_smtp_users(request) - + assert isinstance(response, APIResponse) assert response.status_code == 200 @@ -294,9 +301,9 @@ def test_builder_list_with_custom_limit(self, email_client, test_domain_id): """Test SMTP users list with custom limit using builder.""" builder = SmtpUsersBuilder() request = builder.domain_id(test_domain_id).limit(50).build_smtp_users_list() - + response = email_client.smtp_users.list_smtp_users(request) - + assert isinstance(response, APIResponse) assert response.status_code == 200 @@ -304,10 +311,14 @@ def test_builder_list_with_custom_limit(self, email_client, test_domain_id): def test_builder_get_not_found(self, email_client, test_domain_id): """Test getting non-existent SMTP user using builder.""" from mailersend.exceptions import ResourceNotFoundError - + builder = SmtpUsersBuilder() - request = builder.domain_id(test_domain_id).smtp_user_id("test-smtp-user-id").build_smtp_user_get() - + request = ( + builder.domain_id(test_domain_id) + .smtp_user_id("test-smtp-user-id") + .build_smtp_user_get() + ) + with pytest.raises(ResourceNotFoundError): email_client.smtp_users.get_smtp_user(request) @@ -315,14 +326,15 @@ def test_builder_get_not_found(self, email_client, test_domain_id): def test_builder_create_invalid_domain(self, email_client): """Test creating SMTP user with invalid domain using builder.""" from mailersend.exceptions import ResourceNotFoundError - + builder = SmtpUsersBuilder() - request = (builder - .domain_id("invalid-domain-id") + request = ( + builder.domain_id("invalid-domain-id") .name("Test User") .enabled(True) - .build_smtp_user_create()) - + .build_smtp_user_create() + ) + with pytest.raises(ResourceNotFoundError): email_client.smtp_users.create_smtp_user(request) @@ -330,15 +342,16 @@ def test_builder_create_invalid_domain(self, email_client): def test_builder_update_not_found(self, email_client, test_domain_id): """Test updating non-existent SMTP user using builder.""" from mailersend.exceptions import ResourceNotFoundError - + builder = SmtpUsersBuilder() - request = (builder - .domain_id(test_domain_id) + request = ( + builder.domain_id(test_domain_id) .smtp_user_id("test-smtp-user-id") .name("Updated User") .enabled(False) - .build_smtp_user_update()) - + .build_smtp_user_update() + ) + with pytest.raises(ResourceNotFoundError): email_client.smtp_users.update_smtp_user(request) @@ -346,39 +359,41 @@ def test_builder_update_not_found(self, email_client, test_domain_id): def test_builder_delete_not_found(self, email_client, test_domain_id): """Test deleting non-existent SMTP user using builder.""" from mailersend.exceptions import ResourceNotFoundError - + builder = SmtpUsersBuilder() - request = (builder - .domain_id(test_domain_id) + request = ( + builder.domain_id(test_domain_id) .smtp_user_id("test-smtp-user-id") - .build_smtp_user_delete()) - + .build_smtp_user_delete() + ) + with pytest.raises(ResourceNotFoundError): email_client.smtp_users.delete_smtp_user(request) def test_builder_fluent_interface(self): """Test that builder methods return self for chaining.""" builder = SmtpUsersBuilder() - + # Test method chaining - result = (builder - .domain_id("test-domain") + result = ( + builder.domain_id("test-domain") .smtp_user_id("test-user") .name("Test User") .enabled(True) - .limit(10)) - + .limit(10) + ) + assert result is builder - + # Verify the builder state list_request = builder.build_smtp_users_list() assert list_request.domain_id == "test-domain" assert list_request.query_params.limit == 10 - + get_request = builder.build_smtp_user_get() assert get_request.domain_id == "test-domain" assert get_request.smtp_user_id == "test-user" - + create_request = builder.build_smtp_user_create() assert create_request.domain_id == "test-domain" assert create_request.name == "Test User" @@ -387,35 +402,35 @@ def test_builder_fluent_interface(self): def test_builder_validation_errors(self): """Test builder validation for invalid inputs.""" from mailersend.exceptions import ValidationError - + builder = SmtpUsersBuilder() - + # Test invalid limit (too low) with pytest.raises(ValidationError) as exc_info: builder.limit(5) assert "limit must be between 10 and 100" in str(exc_info.value).lower() - + # Test invalid limit (too high) with pytest.raises(ValidationError) as exc_info: builder.limit(150) assert "limit must be between 10 and 100" in str(exc_info.value).lower() - + # Test invalid page with pytest.raises(ValidationError) as exc_info: builder.page(0) assert "page must be >= 1" in str(exc_info.value).lower() - + # Test building list request without domain_id fresh_builder = SmtpUsersBuilder() with pytest.raises(ValidationError) as exc_info: fresh_builder.build_smtp_users_list() assert "domain id is required" in str(exc_info.value).lower() - + # Test building get request without smtp_user_id with pytest.raises(ValidationError) as exc_info: fresh_builder.domain_id("test").build_smtp_user_get() assert "smtp user id is required" in str(exc_info.value).lower() - + # Test building create request without name with pytest.raises(ValidationError) as exc_info: fresh_builder.domain_id("test").build_smtp_user_create() @@ -424,109 +439,115 @@ def test_builder_validation_errors(self): def test_builder_default_values(self): """Test that builder uses appropriate default values.""" builder = SmtpUsersBuilder() - + # Build request with minimal required fields request = builder.domain_id("test-domain").build_smtp_users_list() - + # Should use default values from the model assert request.query_params.limit == 25 def test_builder_request_variations(self): """Test various request building scenarios with builder.""" builder = SmtpUsersBuilder() - + # Test create request with enabled=True - create_request1 = (builder - .domain_id("test-domain") + create_request1 = ( + builder.domain_id("test-domain") .name("Enabled User") .enabled(True) - .build_smtp_user_create()) + .build_smtp_user_create() + ) assert create_request1.enabled is True - + # Test create request with enabled=False - create_request2 = (builder - .domain_id("test-domain") + create_request2 = ( + builder.domain_id("test-domain") .name("Disabled User") .enabled(False) - .build_smtp_user_create()) + .build_smtp_user_create() + ) assert create_request2.enabled is False - + # Test create request without enabled (should be None) builder_fresh = SmtpUsersBuilder() - create_request3 = (builder_fresh - .domain_id("test-domain") + create_request3 = ( + builder_fresh.domain_id("test-domain") .name("Default User") - .build_smtp_user_create()) + .build_smtp_user_create() + ) assert create_request3.enabled is None def test_builder_json_serialization(self): """Test that builder-created requests serialize correctly to JSON.""" builder = SmtpUsersBuilder() - + # Test create request JSON - create_request = (builder - .domain_id("test-domain") + create_request = ( + builder.domain_id("test-domain") .name("Test User") .enabled(True) - .build_smtp_user_create()) - + .build_smtp_user_create() + ) + json_data = create_request.to_json() assert json_data["name"] == "Test User" assert json_data["enabled"] is True - + # Test create request JSON without enabled builder_fresh = SmtpUsersBuilder() - create_request_no_enabled = (builder_fresh - .domain_id("test-domain") + create_request_no_enabled = ( + builder_fresh.domain_id("test-domain") .name("Test User") - .build_smtp_user_create()) - + .build_smtp_user_create() + ) + json_data_no_enabled = create_request_no_enabled.to_json() assert json_data_no_enabled["name"] == "Test User" assert "enabled" not in json_data_no_enabled - @vcr.use_cassette("smtp_users_comprehensive_workflow.yaml") + @vcr.use_cassette("smtp_users_comprehensive_workflow.yaml") def test_comprehensive_smtp_users_workflow(self, email_client, test_domain_id): """Test comprehensive workflow covering list, CRUD operations, error scenarios, and builder usage.""" # Test list with different configurations list_request = SmtpUsersListRequest( - domain_id=test_domain_id, - query_params=SmtpUsersListQueryParams(limit=10) + domain_id=test_domain_id, query_params=SmtpUsersListQueryParams(limit=10) ) - + response = email_client.smtp_users.list_smtp_users(list_request) assert isinstance(response, APIResponse) assert response.status_code == 200 - + # Test builder pattern with different configurations builder = SmtpUsersBuilder() - + # Test list with builder - builder_request = builder.domain_id(test_domain_id).limit(25).build_smtp_users_list() + builder_request = ( + builder.domain_id(test_domain_id).limit(25).build_smtp_users_list() + ) builder_response = email_client.smtp_users.list_smtp_users(builder_request) assert isinstance(builder_response, APIResponse) assert builder_response.status_code == 200 - + # Test error scenarios from mailersend.exceptions import ResourceNotFoundError - + # Test get non-existent user get_request = SmtpUserGetRequest( - domain_id=test_domain_id, - smtp_user_id="non-existent-id" + domain_id=test_domain_id, smtp_user_id="non-existent-id" ) with pytest.raises(ResourceNotFoundError): email_client.smtp_users.get_smtp_user(get_request) - + # Test create with invalid domain create_request = SmtpUserCreateRequest( - domain_id="invalid-domain", - name="Test User" + domain_id="invalid-domain", name="Test User" ) with pytest.raises(ResourceNotFoundError): email_client.smtp_users.create_smtp_user(create_request) - + # Test builder error scenarios - builder_get_request = builder.smtp_user_id("another-non-existent-id").build_smtp_user_get() + builder_get_request = builder.smtp_user_id( + "another-non-existent-id" + ).build_smtp_user_get() with pytest.raises(ResourceNotFoundError): - email_client.smtp_users.get_smtp_user(builder_get_request) \ No newline at end of file + email_client.smtp_users.get_smtp_user(builder_get_request) diff --git a/tests/integration/test_templates.py b/tests/integration/test_templates.py index a2cc03d..c5c7bb9 100644 --- a/tests/integration/test_templates.py +++ b/tests/integration/test_templates.py @@ -14,9 +14,7 @@ @pytest.fixture def basic_templates_list_request(): """Basic templates list request""" - return TemplatesListRequest( - query_params=TemplatesListQueryParams(page=1, limit=10) - ) + return TemplatesListRequest(query_params=TemplatesListQueryParams(page=1, limit=10)) @pytest.fixture @@ -53,7 +51,9 @@ def test_list_templates_basic(self, email_client, basic_templates_list_request): first_template = templates[0] assert "id" in first_template assert "name" in first_template - assert "category" in first_template or "categories" in first_template # API has both fields + assert ( + "category" in first_template or "categories" in first_template + ) # API has both fields assert "created_at" in first_template @vcr.use_cassette("templates_list_no_params.yaml") @@ -97,9 +97,7 @@ def test_list_templates_with_domain_filter(self, email_client, sample_domain_id) """Test listing templates filtered by domain.""" request = TemplatesListRequest( query_params=TemplatesListQueryParams( - page=1, - limit=10, - domain_id=sample_domain_id + page=1, limit=10, domain_id=sample_domain_id ) ) @@ -117,21 +115,29 @@ def test_list_templates_with_domain_filter(self, email_client, sample_domain_id) assert isinstance(template, dict) @vcr.use_cassette("templates_get_single.yaml") - def test_get_template_not_found_with_test_id(self, email_client, template_get_request): + def test_get_template_not_found_with_test_id( + self, email_client, template_get_request + ): """Test getting a non-existent template returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.templates.get_template(template_get_request) error_str = str(exc_info.value).lower() - assert "not found" in error_str or "404" in error_str or "could not be found" in error_str + assert ( + "not found" in error_str + or "404" in error_str + or "could not be found" in error_str + ) @vcr.use_cassette("templates_delete.yaml") - def test_delete_template_not_found_with_test_id(self, email_client, template_get_request): + def test_delete_template_not_found_with_test_id( + self, email_client, template_get_request + ): """Test deleting a non-existent template returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + delete_request = TemplateDeleteRequest( template_id=template_get_request.template_id ) @@ -140,9 +146,12 @@ def test_delete_template_not_found_with_test_id(self, email_client, template_get email_client.templates.delete_template(delete_request) error_str = str(exc_info.value).lower() - assert ("not found" in error_str or "404" in error_str or - "could not be found" in error_str or - "template" in error_str) + assert ( + "not found" in error_str + or "404" in error_str + or "could not be found" in error_str + or "template" in error_str + ) @vcr.use_cassette("templates_validation_error.yaml") def test_list_templates_validation_error(self, email_client): @@ -153,10 +162,7 @@ def test_list_templates_validation_error(self, email_client): # Should raise an AttributeError for invalid request type error_str = str(exc_info.value).lower() - assert ( - "attribute" in error_str - or "to_query_params" in error_str - ) + assert "attribute" in error_str or "to_query_params" in error_str @vcr.use_cassette("templates_api_response_structure.yaml") def test_api_response_structure(self, email_client, basic_templates_list_request): @@ -180,7 +186,9 @@ def test_api_response_structure(self, email_client, basic_templates_list_request assert len(response.request_id) > 0 @vcr.use_cassette("templates_empty_result.yaml") - def test_list_templates_empty_result(self, email_client, basic_templates_list_request): + def test_list_templates_empty_result( + self, email_client, basic_templates_list_request + ): """Test listing templates when no templates exist.""" response = email_client.templates.list_templates(basic_templates_list_request) @@ -219,23 +227,19 @@ def test_template_delete_model_validation(self): def test_templates_list_query_params_validation(self): """Test validation for templates list query parameters.""" # Test valid parameters - params = TemplatesListQueryParams( - page=1, - limit=25, - domain_id="test-domain" - ) + params = TemplatesListQueryParams(page=1, limit=25, domain_id="test-domain") assert params.page == 1 assert params.limit == 25 assert params.domain_id == "test-domain" - + # Test minimum limit validation with pytest.raises(ValueError): TemplatesListQueryParams(limit=5) # Below minimum of 10 - - # Test maximum limit validation + + # Test maximum limit validation with pytest.raises(ValueError): TemplatesListQueryParams(limit=150) # Above maximum of 100 - + # Test minimum page validation with pytest.raises(ValueError): TemplatesListQueryParams(page=0) # Below minimum of 1 @@ -243,21 +247,17 @@ def test_templates_list_query_params_validation(self): def test_templates_list_query_params_to_dict(self): """Test query parameters conversion to dictionary.""" # Test with all parameters - params = TemplatesListQueryParams( - page=2, - limit=50, - domain_id="test-domain" - ) + params = TemplatesListQueryParams(page=2, limit=50, domain_id="test-domain") query_dict = params.to_query_params() - + assert query_dict["page"] == 2 assert query_dict["limit"] == 50 assert query_dict["domain_id"] == "test-domain" - + # Test with minimal parameters (only defaults) params_minimal = TemplatesListQueryParams() query_dict_minimal = params_minimal.to_query_params() - + assert query_dict_minimal["page"] == 1 assert query_dict_minimal["limit"] == 25 assert "domain_id" not in query_dict_minimal # None values excluded @@ -266,13 +266,13 @@ def test_templates_list_request_defaults(self): """Test that TemplatesListRequest has proper defaults.""" # Test creating request without explicit query_params request = TemplatesListRequest() - + assert request.query_params is not None assert isinstance(request.query_params, TemplatesListQueryParams) assert request.query_params.page == 1 assert request.query_params.limit == 25 assert request.query_params.domain_id is None - + # Test to_query_params method query_dict = request.to_query_params() assert query_dict["page"] == 1 @@ -284,7 +284,7 @@ def test_domain_id_validation_and_cleaning(self): # Test domain_id with whitespace gets cleaned params = TemplatesListQueryParams(domain_id=" test-domain ") assert params.domain_id == "test-domain" - + # Test None domain_id is preserved params_none = TemplatesListQueryParams(domain_id=None) - assert params_none.domain_id is None \ No newline at end of file + assert params_none.domain_id is None diff --git a/tests/integration/test_tokens.py b/tests/integration/test_tokens.py index 5b9248b..2dffb4e 100644 --- a/tests/integration/test_tokens.py +++ b/tests/integration/test_tokens.py @@ -25,9 +25,7 @@ def test_domain_id(): @pytest.fixture def basic_tokens_list_request(): """Basic tokens list request""" - return TokensListRequest( - query_params=TokensListQueryParams(page=1, limit=10) - ) + return TokensListRequest(query_params=TokensListQueryParams(page=1, limit=10)) @pytest.fixture @@ -42,7 +40,7 @@ def sample_token_data(test_domain_id): return { "name": "Test Token", "domain_id": test_domain_id, - "scopes": ["email_full", "domains_read"] + "scopes": ["email_full", "domains_read"], } @@ -116,11 +114,15 @@ def test_list_tokens_different_limit(self, email_client): def test_get_token_not_found(self, email_client, token_get_request): """Test getting a non-existent token returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.tokens.get_token(token_get_request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) or "could not be found" in str(exc_info.value).lower() + assert ( + "not found" in str(exc_info.value).lower() + or "404" in str(exc_info.value) + or "could not be found" in str(exc_info.value).lower() + ) @vcr.use_cassette("tokens_create_basic.yaml") def test_create_token_basic(self, email_client, sample_token_data): @@ -128,7 +130,7 @@ def test_create_token_basic(self, email_client, sample_token_data): request = TokenCreateRequest( name=sample_token_data["name"], domain_id=sample_token_data["domain_id"], - scopes=sample_token_data["scopes"] + scopes=sample_token_data["scopes"], ) response = email_client.tokens.create_token(request) @@ -141,45 +143,51 @@ def test_create_token_basic(self, email_client, sample_token_data): def test_update_token_not_found(self, email_client): """Test updating non-existent token returns 404.""" from mailersend.exceptions import ResourceNotFoundError - - request = TokenUpdateRequest( - token_id="test-token-id", - status="pause" - ) + + request = TokenUpdateRequest(token_id="test-token-id", status="pause") with pytest.raises(ResourceNotFoundError) as exc_info: email_client.tokens.update_token(request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) or "could not be found" in str(exc_info.value).lower() + assert ( + "not found" in str(exc_info.value).lower() + or "404" in str(exc_info.value) + or "could not be found" in str(exc_info.value).lower() + ) @vcr.use_cassette("tokens_update_name_not_found.yaml") def test_update_token_name_not_found(self, email_client): """Test updating non-existent token name returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + request = TokenUpdateNameRequest( - token_id="test-token-id", - name="Updated Token Name" + token_id="test-token-id", name="Updated Token Name" ) with pytest.raises(ResourceNotFoundError) as exc_info: email_client.tokens.update_token_name(request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) or "could not be found" in str(exc_info.value).lower() + assert ( + "not found" in str(exc_info.value).lower() + or "404" in str(exc_info.value) + or "could not be found" in str(exc_info.value).lower() + ) @vcr.use_cassette("tokens_delete_not_found.yaml") def test_delete_token_not_found(self, email_client, token_get_request): """Test deleting non-existent token returns 404.""" from mailersend.exceptions import ResourceNotFoundError - - delete_request = TokenDeleteRequest( - token_id=token_get_request.token_id - ) + + delete_request = TokenDeleteRequest(token_id=token_get_request.token_id) with pytest.raises(ResourceNotFoundError) as exc_info: email_client.tokens.delete_token(delete_request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) or "could not be found" in str(exc_info.value).lower() + assert ( + "not found" in str(exc_info.value).lower() + or "404" in str(exc_info.value) + or "could not be found" in str(exc_info.value).lower() + ) @vcr.use_cassette("tokens_validation_error.yaml") def test_list_tokens_validation_error(self, email_client): @@ -190,10 +198,7 @@ def test_list_tokens_validation_error(self, email_client): # Should raise an AttributeError for invalid request type error_str = str(exc_info.value).lower() - assert ( - "attribute" in error_str - or "to_query_params" in error_str - ) + assert "attribute" in error_str or "to_query_params" in error_str @vcr.use_cassette("tokens_api_response_structure.yaml") def test_api_response_structure(self, email_client, basic_tokens_list_request): @@ -233,38 +238,25 @@ def test_token_create_model_validation(self): """Test model validation for token creation.""" # Test empty name - this will raise a custom validation error from pydantic import ValidationError + with pytest.raises(ValidationError) as exc_info: - TokenCreateRequest( - name="", - domain_id="test-domain", - scopes=["email_full"] - ) + TokenCreateRequest(name="", domain_id="test-domain", scopes=["email_full"]) assert "token name cannot be empty" in str(exc_info.value).lower() # Test empty domain_id with pytest.raises(ValueError) as exc_info: - TokenCreateRequest( - name="Test Token", - domain_id="", - scopes=["email_full"] - ) + TokenCreateRequest(name="Test Token", domain_id="", scopes=["email_full"]) assert "domain id cannot be empty" in str(exc_info.value).lower() # Test empty scopes with pytest.raises(ValidationError) as exc_info: - TokenCreateRequest( - name="Test Token", - domain_id="test-domain", - scopes=[] - ) + TokenCreateRequest(name="Test Token", domain_id="test-domain", scopes=[]) assert "list should have at least 1 item" in str(exc_info.value).lower() # Test invalid scopes with pytest.raises(ValueError) as exc_info: TokenCreateRequest( - name="Test Token", - domain_id="test-domain", - scopes=["invalid_scope"] + name="Test Token", domain_id="test-domain", scopes=["invalid_scope"] ) assert "invalid scopes" in str(exc_info.value).lower() @@ -273,7 +265,7 @@ def test_token_create_model_validation(self): TokenCreateRequest( name="x" * 51, # Exceeds 50 character limit domain_id="test-domain", - scopes=["email_full"] + scopes=["email_full"], ) assert "string should have at most 50 characters" in str(exc_info.value).lower() @@ -282,12 +274,13 @@ def test_token_update_model_validation(self): # Test valid status values valid_request1 = TokenUpdateRequest(token_id="test", status="pause") assert valid_request1.status == "pause" - + valid_request2 = TokenUpdateRequest(token_id="test", status="unpause") assert valid_request2.status == "unpause" # Test invalid status - this will raise a pydantic ValidationError from pydantic import ValidationError + with pytest.raises(ValidationError) as exc_info: TokenUpdateRequest(token_id="test", status="invalid") assert "input should be 'pause' or 'unpause'" in str(exc_info.value).lower() @@ -296,18 +289,15 @@ def test_token_update_name_model_validation(self): """Test model validation for token name updates.""" # Test empty name with pytest.raises(ValueError) as exc_info: - TokenUpdateNameRequest( - token_id="test-id", - name="" - ) + TokenUpdateNameRequest(token_id="test-id", name="") assert "token name cannot be empty" in str(exc_info.value).lower() # Test name too long from pydantic import ValidationError + with pytest.raises(ValidationError) as exc_info: TokenUpdateNameRequest( - token_id="test-id", - name="x" * 51 # Exceeds 50 character limit + token_id="test-id", name="x" * 51 # Exceeds 50 character limit ) assert "string should have at most 50 characters" in str(exc_info.value).lower() @@ -317,15 +307,15 @@ def test_tokens_list_query_params_validation(self): params = TokensListQueryParams(page=1, limit=25) assert params.page == 1 assert params.limit == 25 - + # Test minimum limit validation with pytest.raises(ValueError): TokensListQueryParams(limit=5) # Below minimum of 10 - - # Test maximum limit validation + + # Test maximum limit validation with pytest.raises(ValueError): TokensListQueryParams(limit=150) # Above maximum of 100 - + # Test minimum page validation with pytest.raises(ValueError): TokensListQueryParams(page=0) # Below minimum of 1 @@ -336,11 +326,14 @@ def test_token_scopes_constants(self): assert TOKEN_SCOPES is not None assert isinstance(TOKEN_SCOPES, list) assert len(TOKEN_SCOPES) > 0 - + # Check for some key scopes expected_scopes = [ - "email_full", "domains_read", "domains_full", - "activity_read", "tokens_full" + "email_full", + "domains_read", + "domains_full", + "activity_read", + "tokens_full", ] for scope in expected_scopes: assert scope in TOKEN_SCOPES @@ -351,19 +344,19 @@ def test_token_json_serialization(self): create_request = TokenCreateRequest( name="Test Token", domain_id="test-domain", - scopes=["email_full", "domains_read"] + scopes=["email_full", "domains_read"], ) - + json_data = create_request.to_json() assert json_data["name"] == "Test Token" assert json_data["domain_id"] == "test-domain" assert json_data["scopes"] == ["email_full", "domains_read"] - + # Test update request JSON update_request = TokenUpdateRequest(token_id="test", status="pause") update_json = update_request.to_json() assert update_json["status"] == "pause" - + # Test update name request JSON update_name_request = TokenUpdateNameRequest(token_id="test", name="New Name") update_name_json = update_name_request.to_json() @@ -378,9 +371,9 @@ def test_builder_list_basic_usage(self, email_client): """Test basic tokens list using builder.""" builder = TokensBuilder() request = builder.page(1).limit(10).build_tokens_list() - + response = email_client.tokens.list_tokens(request) - + assert isinstance(response, APIResponse) assert response.status_code == 200 @@ -389,12 +382,12 @@ def test_builder_list_with_custom_limit(self, email_client): """Test tokens list with custom limit using builder.""" builder = TokensBuilder() request = builder.page(1).limit(50).build_tokens_list() - + response = email_client.tokens.list_tokens(request) - + assert isinstance(response, APIResponse) assert response.status_code == 200 - + # Check that the limit was applied if "meta" in response.data: meta = response.data["meta"] @@ -404,10 +397,10 @@ def test_builder_list_with_custom_limit(self, email_client): def test_builder_get_not_found(self, email_client): """Test getting non-existent token using builder.""" from mailersend.exceptions import ResourceNotFoundError - + builder = TokensBuilder() request = builder.token_id("test-token-id").build_token_get() - + with pytest.raises(ResourceNotFoundError): email_client.tokens.get_token(request) @@ -415,14 +408,15 @@ def test_builder_get_not_found(self, email_client): def test_builder_create_basic(self, email_client, test_domain_id): """Test creating token using builder.""" builder = TokensBuilder() - request = (builder - .name("Test Token") + request = ( + builder.name("Test Token") .domain_id(test_domain_id) .add_scope("email_full") - .build_token_create()) - + .build_token_create() + ) + response = email_client.tokens.create_token(request) - + assert isinstance(response, APIResponse) assert response.status_code in [200, 201] assert response.data is not None @@ -431,13 +425,10 @@ def test_builder_create_basic(self, email_client, test_domain_id): def test_builder_update_not_found(self, email_client): """Test updating non-existent token using builder.""" from mailersend.exceptions import ResourceNotFoundError - + builder = TokensBuilder() - request = (builder - .token_id("test-token-id") - .pause() - .build_token_update()) - + request = builder.token_id("test-token-id").pause().build_token_update() + with pytest.raises(ResourceNotFoundError): email_client.tokens.update_token(request) @@ -445,13 +436,14 @@ def test_builder_update_not_found(self, email_client): def test_builder_update_name_not_found(self, email_client): """Test updating non-existent token name using builder.""" from mailersend.exceptions import ResourceNotFoundError - + builder = TokensBuilder() - request = (builder - .token_id("test-token-id") + request = ( + builder.token_id("test-token-id") .name("Updated Name") - .build_token_update_name()) - + .build_token_update_name() + ) + with pytest.raises(ResourceNotFoundError): email_client.tokens.update_token_name(request) @@ -459,37 +451,38 @@ def test_builder_update_name_not_found(self, email_client): def test_builder_delete_not_found(self, email_client): """Test deleting non-existent token using builder.""" from mailersend.exceptions import ResourceNotFoundError - + builder = TokensBuilder() request = builder.token_id("test-token-id").build_token_delete() - + with pytest.raises(ResourceNotFoundError): email_client.tokens.delete_token(request) def test_builder_fluent_interface(self): """Test that builder methods return self for chaining.""" builder = TokensBuilder() - + # Test method chaining - result = (builder - .page(1) + result = ( + builder.page(1) .limit(10) .token_id("test-token") .name("Test Token") .domain_id("test-domain") .add_scope("email_full") - .status("pause")) - + .status("pause") + ) + assert result is builder - + # Verify the builder state for different requests list_request = builder.build_tokens_list() assert list_request.query_params.page == 1 assert list_request.query_params.limit == 10 - + get_request = builder.build_token_get() assert get_request.token_id == "test-token" - + create_request = builder.build_token_create() assert create_request.name == "Test Token" assert create_request.domain_id == "test-domain" @@ -498,30 +491,30 @@ def test_builder_fluent_interface(self): def test_builder_scope_helpers(self): """Test builder scope helper methods.""" builder = TokensBuilder() - + # Test individual scope helpers builder.email_scopes() assert "email_full" in builder._scopes - + builder.domains_read_scope() assert "domains_read" in builder._scopes - + builder.domains_full_scope() assert "domains_full" in builder._scopes - + # Test scope groups builder_fresh = TokensBuilder() builder_fresh.activity_scopes() assert "activity_read" in builder_fresh._scopes assert "activity_full" in builder_fresh._scopes - + # Test all read scopes builder_read = TokensBuilder() builder_read.all_read_scopes() read_scopes = ["domains_read", "activity_read", "analytics_read"] for scope in read_scopes: assert scope in builder_read._scopes - + # Test all scopes builder_all = TokensBuilder() builder_all.all_scopes() @@ -531,11 +524,11 @@ def test_builder_scope_helpers(self): def test_builder_status_helpers(self): """Test builder status helper methods.""" builder = TokensBuilder() - + # Test pause helper builder.pause() assert builder._status == "pause" - + # Test unpause helper builder.unpause() assert builder._status == "unpause" @@ -544,10 +537,10 @@ def test_builder_reset_functionality(self): """Test builder reset functionality.""" builder = TokensBuilder() builder.page(2).limit(50).token_id("test").name("test").add_scope("email_full") - + # Reset the builder builder.reset() - + # Verify all fields are cleared assert builder._page is None assert builder._limit is None @@ -561,13 +554,13 @@ def test_builder_copy_functionality(self): """Test builder copy functionality.""" original_builder = TokensBuilder() original_builder.page(2).limit(50).add_scope("email_full") - + # Copy the builder copied_builder = original_builder.copy() - + # Modify the copy copied_builder.page(3).add_scope("domains_read") - + # Verify original is unchanged assert original_builder._page == 2 assert copied_builder._page == 3 @@ -579,27 +572,27 @@ def test_builder_copy_functionality(self): def test_builder_validation_errors(self): """Test builder validation for missing required fields.""" builder = TokensBuilder() - + # Test building get request without token_id with pytest.raises(ValueError) as exc_info: builder.build_token_get() assert "token_id is required" in str(exc_info.value).lower() - + # Test building create request without name with pytest.raises(ValueError) as exc_info: builder.domain_id("test").add_scope("email_full").build_token_create() assert "name is required" in str(exc_info.value).lower() - + # Test building create request without domain_id with pytest.raises(ValueError) as exc_info: builder.reset().name("test").add_scope("email_full").build_token_create() assert "domain_id is required" in str(exc_info.value).lower() - + # Test building create request without scopes with pytest.raises(ValueError) as exc_info: builder.reset().name("test").domain_id("test").build_token_create() assert "scopes are required" in str(exc_info.value).lower() - + # Test building update request without status with pytest.raises(ValueError) as exc_info: builder.reset().token_id("test").build_token_update() @@ -608,13 +601,13 @@ def test_builder_validation_errors(self): def test_builder_scope_deduplication(self): """Test that builder prevents duplicate scopes.""" builder = TokensBuilder() - + # Add the same scope multiple times builder.add_scope("email_full") builder.add_scope("email_full") builder.add_scope("domains_read") builder.add_scope("email_full") - + # Should only have unique scopes assert builder._scopes.count("email_full") == 1 assert builder._scopes.count("domains_read") == 1 @@ -623,48 +616,46 @@ def test_builder_scope_deduplication(self): def test_builder_default_values(self): """Test that builder uses appropriate default values.""" builder = TokensBuilder() - + # Build request without setting pagination values request = builder.build_tokens_list() - + # Should use default values from the model assert request.query_params.page == 1 assert request.query_params.limit == 25 - @vcr.use_cassette("tokens_comprehensive_workflow.yaml") + @vcr.use_cassette("tokens_comprehensive_workflow.yaml") def test_comprehensive_tokens_workflow(self, email_client, test_domain_id): """Test comprehensive workflow covering list, CRUD operations, error scenarios, and builder usage.""" # Test list with different configurations list_request = TokensListRequest( query_params=TokensListQueryParams(page=1, limit=10) ) - + response = email_client.tokens.list_tokens(list_request) assert isinstance(response, APIResponse) assert response.status_code == 200 - + # Test builder pattern with different configurations builder = TokensBuilder() - + # Test list with builder builder_request = builder.page(1).limit(25).build_tokens_list() builder_response = email_client.tokens.list_tokens(builder_request) assert isinstance(builder_response, APIResponse) assert builder_response.status_code == 200 - + # Test error scenarios from mailersend.exceptions import ResourceNotFoundError - + # Test get non-existent token get_request = TokenGetRequest(token_id="non-existent-id") with pytest.raises(ResourceNotFoundError): email_client.tokens.get_token(get_request) - + # Test create token create_request = TokenCreateRequest( - name="Test Token", - domain_id=test_domain_id, - scopes=["email_full"] + name="Test Token", domain_id=test_domain_id, scopes=["email_full"] ) try: create_response = email_client.tokens.create_token(create_request) @@ -672,8 +663,10 @@ def test_comprehensive_tokens_workflow(self, email_client, test_domain_id): except Exception: # Creation might fail due to quota limits or other reasons pass - + # Test builder error scenarios - builder_get_request = builder.token_id("another-non-existent-id").build_token_get() + builder_get_request = builder.token_id( + "another-non-existent-id" + ).build_token_get() with pytest.raises(ResourceNotFoundError): - email_client.tokens.get_token(builder_get_request) \ No newline at end of file + email_client.tokens.get_token(builder_get_request) diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py index 6f124ec..57109b0 100644 --- a/tests/integration/test_users.py +++ b/tests/integration/test_users.py @@ -21,17 +21,13 @@ @pytest.fixture def basic_users_list_request(): """Basic users list request""" - return UsersListRequest( - query_params=UsersListQueryParams(page=1, limit=10) - ) + return UsersListRequest(query_params=UsersListQueryParams(page=1, limit=10)) @pytest.fixture def basic_invites_list_request(): """Basic invites list request""" - return InvitesListRequest( - query_params=InvitesListQueryParams(page=1, limit=10) - ) + return InvitesListRequest(query_params=InvitesListQueryParams(page=1, limit=10)) @pytest.fixture @@ -55,7 +51,7 @@ def sample_user_invite_data(): "permissions": ["email_send"], "domains": [], "templates": [], - "requires_periodic_password_change": False + "requires_periodic_password_change": False, } @@ -91,9 +87,7 @@ def test_list_users_basic(self, email_client, basic_users_list_request): @vcr.use_cassette("users_list_with_pagination.yaml") def test_list_users_with_pagination(self, email_client): """Test listing users with pagination.""" - request = UsersListRequest( - query_params=UsersListQueryParams(page=1, limit=10) - ) + request = UsersListRequest(query_params=UsersListQueryParams(page=1, limit=10)) response = email_client.users.list_users(request) @@ -113,20 +107,23 @@ def test_list_users_with_pagination(self, email_client): def test_get_user_not_found_with_test_id(self, email_client, user_get_request): """Test getting a non-existent user returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.users.get_user(user_get_request) error_str = str(exc_info.value).lower() - assert ("not found" in error_str or "404" in error_str or - "could not be found" in error_str or - "user" in error_str) + assert ( + "not found" in error_str + or "404" in error_str + or "could not be found" in error_str + or "user" in error_str + ) @vcr.use_cassette("users_invite.yaml") def test_invite_user_invalid_data(self, email_client, sample_user_invite_data): """Test inviting a user with potentially invalid data.""" from mailersend.exceptions import BadRequestError, ResourceNotFoundError - + request = UserInviteRequest(**sample_user_invite_data) # This might fail due to invalid role, domain restrictions, or other business logic @@ -134,44 +131,55 @@ def test_invite_user_invalid_data(self, email_client, sample_user_invite_data): email_client.users.invite_user(request) error_str = str(exc_info.value).lower() - assert ("invalid" in error_str or "not found" in error_str or - "role" in error_str or "permission" in error_str or - "domain" in error_str) + assert ( + "invalid" in error_str + or "not found" in error_str + or "role" in error_str + or "permission" in error_str + or "domain" in error_str + ) @vcr.use_cassette("users_update.yaml") def test_update_user_not_found_with_test_id(self, email_client, user_get_request): """Test updating a non-existent user with invalid role/permissions.""" from mailersend.exceptions import BadRequestError - + update_request = UserUpdateRequest( user_id=user_get_request.user_id, role="member", # Invalid role - API validates before checking user existence permissions=["email_send"], # Invalid permission domains=[], - templates=[] + templates=[], ) with pytest.raises(BadRequestError) as exc_info: email_client.users.update_user(update_request) error_str = str(exc_info.value).lower() - assert ("invalid" in error_str or "role" in error_str or - "permission" in error_str or "selected" in error_str) + assert ( + "invalid" in error_str + or "role" in error_str + or "permission" in error_str + or "selected" in error_str + ) @vcr.use_cassette("users_delete.yaml") def test_delete_user_not_found_with_test_id(self, email_client, user_get_request): """Test deleting a non-existent user returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + delete_request = UserDeleteRequest(user_id=user_get_request.user_id) with pytest.raises(ResourceNotFoundError) as exc_info: email_client.users.delete_user(delete_request) error_str = str(exc_info.value).lower() - assert ("not found" in error_str or "404" in error_str or - "could not be found" in error_str or - "user" in error_str) + assert ( + "not found" in error_str + or "404" in error_str + or "could not be found" in error_str + or "user" in error_str + ) # ============================================================================ # Invite Management Tests @@ -203,44 +211,57 @@ def test_list_invites_basic(self, email_client, basic_invites_list_request): def test_get_invite_not_found_with_test_id(self, email_client, invite_get_request): """Test getting a non-existent invite returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.users.get_invite(invite_get_request) error_str = str(exc_info.value).lower() - assert ("not found" in error_str or "404" in error_str or - "could not be found" in error_str or - "invite" in error_str) + assert ( + "not found" in error_str + or "404" in error_str + or "could not be found" in error_str + or "invite" in error_str + ) @vcr.use_cassette("invites_resend.yaml") - def test_resend_invite_not_found_with_test_id(self, email_client, invite_get_request): + def test_resend_invite_not_found_with_test_id( + self, email_client, invite_get_request + ): """Test resending a non-existent invite returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + resend_request = InviteResendRequest(invite_id=invite_get_request.invite_id) with pytest.raises(ResourceNotFoundError) as exc_info: email_client.users.resend_invite(resend_request) error_str = str(exc_info.value).lower() - assert ("not found" in error_str or "404" in error_str or - "could not be found" in error_str or - "invite" in error_str) + assert ( + "not found" in error_str + or "404" in error_str + or "could not be found" in error_str + or "invite" in error_str + ) @vcr.use_cassette("invites_cancel.yaml") - def test_cancel_invite_not_found_with_test_id(self, email_client, invite_get_request): + def test_cancel_invite_not_found_with_test_id( + self, email_client, invite_get_request + ): """Test canceling a non-existent invite returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + cancel_request = InviteCancelRequest(invite_id=invite_get_request.invite_id) with pytest.raises(ResourceNotFoundError) as exc_info: email_client.users.cancel_invite(cancel_request) error_str = str(exc_info.value).lower() - assert ("not found" in error_str or "404" in error_str or - "could not be found" in error_str or - "invite" in error_str) + assert ( + "not found" in error_str + or "404" in error_str + or "could not be found" in error_str + or "invite" in error_str + ) # ============================================================================ # Validation and API Response Tests @@ -255,10 +276,7 @@ def test_list_users_validation_error(self, email_client): # Should raise an AttributeError for invalid request type error_str = str(exc_info.value).lower() - assert ( - "attribute" in error_str - or "to_query_params" in error_str - ) + assert "attribute" in error_str or "to_query_params" in error_str @vcr.use_cassette("users_api_response_structure.yaml") def test_api_response_structure(self, email_client, basic_users_list_request): @@ -338,15 +356,15 @@ def test_users_list_query_params_validation(self): params = UsersListQueryParams(page=1, limit=25) assert params.page == 1 assert params.limit == 25 - + # Test minimum limit validation with pytest.raises(ValueError): UsersListQueryParams(limit=5) # Below minimum of 10 - - # Test maximum limit validation + + # Test maximum limit validation with pytest.raises(ValueError): UsersListQueryParams(limit=150) # Above maximum of 100 - + # Test minimum page validation with pytest.raises(ValueError): UsersListQueryParams(page=0) # Below minimum of 1 @@ -357,15 +375,15 @@ def test_invites_list_query_params_validation(self): params = InvitesListQueryParams(page=2, limit=50) assert params.page == 2 assert params.limit == 50 - + # Test minimum limit validation with pytest.raises(ValueError): InvitesListQueryParams(limit=5) # Below minimum of 10 - - # Test maximum limit validation + + # Test maximum limit validation with pytest.raises(ValueError): InvitesListQueryParams(limit=150) # Above maximum of 100 - + # Test minimum page validation with pytest.raises(ValueError): InvitesListQueryParams(page=0) # Below minimum of 1 @@ -378,11 +396,11 @@ def test_user_invite_to_json(self): permissions=["email_send", "domains_manage"], domains=["domain1", "domain2"], templates=["template1"], - requires_periodic_password_change=True + requires_periodic_password_change=True, ) - + json_data = request.to_json() - + assert json_data["email"] == "test@example.com" assert json_data["role"] == "admin" assert json_data["permissions"] == ["email_send", "domains_manage"] @@ -398,11 +416,11 @@ def test_user_update_to_json(self): permissions=["email_send"], domains=["domain1"], templates=[], - requires_periodic_password_change=False + requires_periodic_password_change=False, ) - + json_data = request.to_json() - + assert "user_id" not in json_data # user_id goes in URL, not body assert json_data["role"] == "member" assert json_data["permissions"] == ["email_send"] @@ -415,7 +433,7 @@ def test_email_validation_and_cleaning(self): # Test email with whitespace gets cleaned request = UserInviteRequest(email=" test@example.com ", role="member") assert request.email == "test@example.com" - + # Test role with whitespace gets cleaned request2 = UserInviteRequest(email="test@example.com", role=" admin ") - assert request2.role == "admin" \ No newline at end of file + assert request2.role == "admin" diff --git a/tests/integration/test_webhooks.py b/tests/integration/test_webhooks.py index b290e84..6f0bdd0 100644 --- a/tests/integration/test_webhooks.py +++ b/tests/integration/test_webhooks.py @@ -42,7 +42,7 @@ def sample_webhook_data(test_domain_id): "name": "Test Webhook", "events": ["activity.sent", "activity.delivered"], "domain_id": test_domain_id, - "enabled": True + "enabled": True, } @@ -77,7 +77,7 @@ def test_list_webhooks_basic(self, email_client, basic_webhooks_list_request): def test_list_webhooks_invalid_domain(self, email_client): """Test listing webhooks with invalid domain ID returns error.""" from mailersend.exceptions import BadRequestError - + request = WebhooksListRequest( query_params=WebhooksListQueryParams(domain_id="invalid-domain-id") ) @@ -92,11 +92,15 @@ def test_list_webhooks_invalid_domain(self, email_client): def test_get_webhook_not_found(self, email_client, webhook_get_request): """Test getting a non-existent webhook returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + with pytest.raises(ResourceNotFoundError) as exc_info: email_client.webhooks.get_webhook(webhook_get_request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) or "could not be found" in str(exc_info.value).lower() + assert ( + "not found" in str(exc_info.value).lower() + or "404" in str(exc_info.value) + or "could not be found" in str(exc_info.value).lower() + ) @vcr.use_cassette("webhooks_create_basic.yaml") def test_create_webhook_basic(self, email_client, sample_webhook_data): @@ -113,7 +117,7 @@ def test_create_webhook_basic(self, email_client, sample_webhook_data): def test_create_webhook_invalid_domain(self, email_client, sample_webhook_data): """Test creating webhook with invalid domain ID returns error.""" from mailersend.exceptions import BadRequestError - + webhook_data = sample_webhook_data.copy() webhook_data["domain_id"] = "invalid-domain-id" request = WebhookCreateRequest(**webhook_data) @@ -122,37 +126,43 @@ def test_create_webhook_invalid_domain(self, email_client, sample_webhook_data): email_client.webhooks.create_webhook(request) error_str = str(exc_info.value).lower() - assert "domain" in error_str or "not found" in error_str or "invalid" in error_str + assert ( + "domain" in error_str or "not found" in error_str or "invalid" in error_str + ) @vcr.use_cassette("webhooks_update_not_found.yaml") def test_update_webhook_not_found(self, email_client): """Test updating non-existent webhook returns 404.""" from mailersend.exceptions import ResourceNotFoundError - + request = WebhookUpdateRequest( - webhook_id="test-webhook-id", - name="Updated Webhook Name", - enabled=False + webhook_id="test-webhook-id", name="Updated Webhook Name", enabled=False ) with pytest.raises(ResourceNotFoundError) as exc_info: email_client.webhooks.update_webhook(request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) or "could not be found" in str(exc_info.value).lower() + assert ( + "not found" in str(exc_info.value).lower() + or "404" in str(exc_info.value) + or "could not be found" in str(exc_info.value).lower() + ) @vcr.use_cassette("webhooks_delete_not_found.yaml") def test_delete_webhook_not_found(self, email_client, webhook_get_request): """Test deleting non-existent webhook returns 404.""" from mailersend.exceptions import ResourceNotFoundError - - delete_request = WebhookDeleteRequest( - webhook_id=webhook_get_request.webhook_id - ) + + delete_request = WebhookDeleteRequest(webhook_id=webhook_get_request.webhook_id) with pytest.raises(ResourceNotFoundError) as exc_info: email_client.webhooks.delete_webhook(delete_request) - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) or "could not be found" in str(exc_info.value).lower() + assert ( + "not found" in str(exc_info.value).lower() + or "404" in str(exc_info.value) + or "could not be found" in str(exc_info.value).lower() + ) @vcr.use_cassette("webhooks_validation_error.yaml") def test_list_webhooks_validation_error(self, email_client): @@ -163,10 +173,7 @@ def test_list_webhooks_validation_error(self, email_client): # Should raise an AttributeError for invalid request type error_str = str(exc_info.value).lower() - assert ( - "attribute" in error_str - or "to_query_params" in error_str - ) + assert "attribute" in error_str or "to_query_params" in error_str @vcr.use_cassette("webhooks_api_response_structure.yaml") def test_api_response_structure(self, email_client, basic_webhooks_list_request): @@ -190,7 +197,9 @@ def test_api_response_structure(self, email_client, basic_webhooks_list_request) assert len(response.request_id) > 0 @vcr.use_cassette("webhooks_empty_result.yaml") - def test_list_webhooks_empty_result(self, email_client, basic_webhooks_list_request): + def test_list_webhooks_empty_result( + self, email_client, basic_webhooks_list_request + ): """Test listing webhooks when no webhooks exist.""" response = email_client.webhooks.list_webhooks(basic_webhooks_list_request) @@ -210,7 +219,7 @@ def test_webhook_create_model_validation(self): url="", name="Test Webhook", events=["activity.sent"], - domain_id="test-domain" + domain_id="test-domain", ) assert "url cannot be empty" in str(exc_info.value).lower() @@ -220,7 +229,7 @@ def test_webhook_create_model_validation(self): url="https://example.com/webhook", name="", events=["activity.sent"], - domain_id="test-domain" + domain_id="test-domain", ) assert "name cannot be empty" in str(exc_info.value).lower() @@ -230,7 +239,7 @@ def test_webhook_create_model_validation(self): url="https://example.com/webhook", name="Test Webhook", events=[], - domain_id="test-domain" + domain_id="test-domain", ) assert "events cannot be empty" in str(exc_info.value).lower() @@ -240,20 +249,23 @@ def test_webhook_create_model_validation(self): url="https://example.com/webhook", name="Test Webhook", events=["activity.sent"], - domain_id="" + domain_id="", ) assert "domain_id cannot be empty" in str(exc_info.value).lower() # Test URL too long from pydantic import ValidationError + with pytest.raises(ValidationError) as exc_info: WebhookCreateRequest( url="x" * 192, # Exceeds 191 character limit name="Test Webhook", events=["activity.sent"], - domain_id="test-domain" + domain_id="test-domain", ) - assert "string should have at most 191 characters" in str(exc_info.value).lower() + assert ( + "string should have at most 191 characters" in str(exc_info.value).lower() + ) # Test name too long with pytest.raises(ValidationError) as exc_info: @@ -261,42 +273,32 @@ def test_webhook_create_model_validation(self): url="https://example.com/webhook", name="x" * 192, # Exceeds 191 character limit events=["activity.sent"], - domain_id="test-domain" + domain_id="test-domain", ) - assert "string should have at most 191 characters" in str(exc_info.value).lower() + assert ( + "string should have at most 191 characters" in str(exc_info.value).lower() + ) def test_webhook_update_model_validation(self): """Test model validation for webhook updates.""" # Test empty webhook_id with pytest.raises(ValueError) as exc_info: - WebhookUpdateRequest( - webhook_id="", - name="Updated Name" - ) + WebhookUpdateRequest(webhook_id="", name="Updated Name") assert "webhook_id cannot be empty" in str(exc_info.value).lower() # Test empty URL when provided with pytest.raises(ValueError) as exc_info: - WebhookUpdateRequest( - webhook_id="test-id", - url="" - ) + WebhookUpdateRequest(webhook_id="test-id", url="") assert "url cannot be empty when provided" in str(exc_info.value).lower() # Test empty name when provided with pytest.raises(ValueError) as exc_info: - WebhookUpdateRequest( - webhook_id="test-id", - name="" - ) + WebhookUpdateRequest(webhook_id="test-id", name="") assert "name cannot be empty when provided" in str(exc_info.value).lower() # Test empty events when provided with pytest.raises(ValueError) as exc_info: - WebhookUpdateRequest( - webhook_id="test-id", - events=[] - ) + WebhookUpdateRequest(webhook_id="test-id", events=[]) assert "events cannot be empty when provided" in str(exc_info.value).lower() def test_webhook_get_model_validation(self): @@ -318,7 +320,7 @@ def test_webhooks_list_query_params_validation(self): # Test valid parameters params = WebhooksListQueryParams(domain_id="test-domain") assert params.domain_id == "test-domain" - + # Test empty domain_id with pytest.raises(ValueError) as exc_info: WebhooksListQueryParams(domain_id="") @@ -333,9 +335,9 @@ def test_builder_list_basic_usage(self, email_client, test_domain_id): """Test basic webhooks list using builder.""" builder = WebhooksBuilder() request = builder.domain_id(test_domain_id).build_webhooks_list_request() - + response = email_client.webhooks.list_webhooks(request) - + assert isinstance(response, APIResponse) assert response.status_code == 200 @@ -343,10 +345,10 @@ def test_builder_list_basic_usage(self, email_client, test_domain_id): def test_builder_get_not_found(self, email_client): """Test getting non-existent webhook using builder.""" from mailersend.exceptions import ResourceNotFoundError - + builder = WebhooksBuilder() request = builder.webhook_id("test-webhook-id").build_webhook_get_request() - + with pytest.raises(ResourceNotFoundError): email_client.webhooks.get_webhook(request) @@ -354,15 +356,16 @@ def test_builder_get_not_found(self, email_client): def test_builder_create_basic(self, email_client, test_domain_id): """Test creating webhook using builder.""" builder = WebhooksBuilder() - request = (builder - .domain_id(test_domain_id) + request = ( + builder.domain_id(test_domain_id) .name("Test Webhook Builder") .url("https://example.com/webhook-builder") .add_event("activity.sent") .add_event("activity.delivered") .enabled(True) - .build_webhook_create_request()) - + .build_webhook_create_request() + ) + try: response = email_client.webhooks.create_webhook(request) assert isinstance(response, APIResponse) @@ -377,14 +380,15 @@ def test_builder_create_basic(self, email_client, test_domain_id): def test_builder_update_not_found(self, email_client): """Test updating non-existent webhook using builder.""" from mailersend.exceptions import ResourceNotFoundError - + builder = WebhooksBuilder() - request = (builder - .webhook_id("test-webhook-id") + request = ( + builder.webhook_id("test-webhook-id") .name("Updated Name") .enabled(False) - .build_webhook_update_request()) - + .build_webhook_update_request() + ) + with pytest.raises(ResourceNotFoundError): email_client.webhooks.update_webhook(request) @@ -392,36 +396,37 @@ def test_builder_update_not_found(self, email_client): def test_builder_delete_not_found(self, email_client): """Test deleting non-existent webhook using builder.""" from mailersend.exceptions import ResourceNotFoundError - + builder = WebhooksBuilder() request = builder.webhook_id("test-webhook-id").build_webhook_delete_request() - + with pytest.raises(ResourceNotFoundError): email_client.webhooks.delete_webhook(request) def test_builder_fluent_interface(self): """Test that builder methods return self for chaining.""" builder = WebhooksBuilder() - + # Test method chaining - result = (builder - .domain_id("test-domain") + result = ( + builder.domain_id("test-domain") .webhook_id("test-webhook") .name("Test Webhook") .url("https://example.com/webhook") .add_event("activity.sent") - .enabled(True)) - + .enabled(True) + ) + assert result is builder - + # Verify the builder state for list request list_request = builder.build_webhooks_list_request() assert list_request.query_params.domain_id == "test-domain" - + # Verify the builder state for get request get_request = builder.build_webhook_get_request() assert get_request.webhook_id == "test-webhook" - + # Verify the builder state for create request create_request = builder.build_webhook_create_request() assert create_request.name == "Test Webhook" @@ -432,29 +437,41 @@ def test_builder_fluent_interface(self): def test_builder_event_helpers(self): """Test builder event helper methods.""" builder = WebhooksBuilder() - + # Test activity events builder.activity_events() activity_events = [ - "activity.sent", "activity.delivered", "activity.soft_bounced", - "activity.hard_bounced", "activity.opened", "activity.opened_unique", - "activity.clicked", "activity.clicked_unique", "activity.unsubscribed", - "activity.spam_complaint", "activity.survey_opened", "activity.survey_submitted" + "activity.sent", + "activity.delivered", + "activity.soft_bounced", + "activity.hard_bounced", + "activity.opened", + "activity.opened_unique", + "activity.clicked", + "activity.clicked_unique", + "activity.unsubscribed", + "activity.spam_complaint", + "activity.survey_opened", + "activity.survey_submitted", ] for event in activity_events: assert event in builder._events - + # Test system events builder_system = WebhooksBuilder() builder_system.system_events() system_events = [ - "sender_identity.verified", "maintenance.start", "maintenance.end", - "inbound_forward.failed", "email_single.verified", "email_list.verified", - "bulk_email.completed" + "sender_identity.verified", + "maintenance.start", + "maintenance.end", + "inbound_forward.failed", + "email_single.verified", + "email_list.verified", + "bulk_email.completed", ] for event in system_events: assert event in builder_system._events - + # Test all events builder_all = WebhooksBuilder() builder_all.all_events() @@ -465,13 +482,13 @@ def test_builder_event_helpers(self): def test_builder_event_deduplication(self): """Test that builder prevents duplicate events.""" builder = WebhooksBuilder() - + # Add the same event multiple times builder.add_event("activity.sent") builder.add_event("activity.sent") builder.add_event("activity.delivered") builder.add_event("activity.sent") - + # Should only have unique events assert builder._events.count("activity.sent") == 1 assert builder._events.count("activity.delivered") == 1 @@ -480,11 +497,13 @@ def test_builder_event_deduplication(self): def test_builder_reset_functionality(self): """Test builder reset functionality.""" builder = WebhooksBuilder() - builder.domain_id("test").webhook_id("test").name("test").add_event("activity.sent") - + builder.domain_id("test").webhook_id("test").name("test").add_event( + "activity.sent" + ) + # Reset the builder builder.reset() - + # Verify all fields are cleared assert builder._domain_id is None assert builder._webhook_id is None @@ -497,13 +516,13 @@ def test_builder_copy_functionality(self): """Test builder copy functionality.""" original_builder = WebhooksBuilder() original_builder.domain_id("test").add_event("activity.sent").enabled(True) - + # Copy the builder copied_builder = original_builder.copy() - + # Modify the copy copied_builder.domain_id("different").add_event("activity.delivered") - + # Verify original is unchanged assert original_builder._domain_id == "test" assert copied_builder._domain_id == "different" @@ -517,42 +536,50 @@ def test_builder_copy_functionality(self): def test_builder_validation_errors(self): """Test builder validation for missing required fields.""" builder = WebhooksBuilder() - + # Test building list request without domain_id with pytest.raises(ValueError) as exc_info: builder.build_webhooks_list_request() assert "domain_id is required" in str(exc_info.value).lower() - + # Test building get request without webhook_id with pytest.raises(ValueError) as exc_info: builder.build_webhook_get_request() assert "webhook_id is required" in str(exc_info.value).lower() - + # Test building create request without URL with pytest.raises(ValueError) as exc_info: - builder.domain_id("test").name("test").add_event("activity.sent").build_webhook_create_request() + builder.domain_id("test").name("test").add_event( + "activity.sent" + ).build_webhook_create_request() assert "url is required" in str(exc_info.value).lower() - + # Test building create request without name with pytest.raises(ValueError) as exc_info: - builder.reset().domain_id("test").url("https://test.com").add_event("activity.sent").build_webhook_create_request() + builder.reset().domain_id("test").url("https://test.com").add_event( + "activity.sent" + ).build_webhook_create_request() assert "name is required" in str(exc_info.value).lower() - + # Test building create request without events with pytest.raises(ValueError) as exc_info: - builder.reset().domain_id("test").url("https://test.com").name("test").build_webhook_create_request() + builder.reset().domain_id("test").url("https://test.com").name( + "test" + ).build_webhook_create_request() assert "events are required" in str(exc_info.value).lower() - + # Test building create request without domain_id with pytest.raises(ValueError) as exc_info: - builder.reset().url("https://test.com").name("test").add_event("activity.sent").build_webhook_create_request() + builder.reset().url("https://test.com").name("test").add_event( + "activity.sent" + ).build_webhook_create_request() assert "domain_id is required" in str(exc_info.value).lower() - + # Test building update request without webhook_id with pytest.raises(ValueError) as exc_info: builder.reset().name("test").build_webhook_update_request() assert "webhook_id is required" in str(exc_info.value).lower() - + # Test building delete request without webhook_id with pytest.raises(ValueError) as exc_info: builder.reset().build_webhook_delete_request() @@ -561,16 +588,16 @@ def test_builder_validation_errors(self): def test_builder_events_list_management(self): """Test builder events list management.""" builder = WebhooksBuilder() - + # Test events setter builder.events(["activity.sent", "activity.delivered"]) assert builder._events == ["activity.sent", "activity.delivered"] - + # Test add_event method builder.add_event("activity.opened") assert "activity.opened" in builder._events assert len(builder._events) == 3 - + # Test that add_event doesn't add duplicates builder.add_event("activity.sent") assert builder._events.count("activity.sent") == 1 @@ -578,16 +605,17 @@ def test_builder_events_list_management(self): def test_builder_webhook_model_serialization(self): """Test that builder-created requests serialize correctly.""" builder = WebhooksBuilder() - + # Test create request serialization - create_request = (builder - .domain_id("test-domain") + create_request = ( + builder.domain_id("test-domain") .name("Test Webhook") .url("https://example.com/webhook") .events(["activity.sent", "activity.delivered"]) .enabled(True) - .build_webhook_create_request()) - + .build_webhook_create_request() + ) + # Verify serialization via model_dump data = create_request.model_dump(exclude_none=True) assert data["domain_id"] == "test-domain" @@ -595,55 +623,60 @@ def test_builder_webhook_model_serialization(self): assert data["url"] == "https://example.com/webhook" assert data["events"] == ["activity.sent", "activity.delivered"] assert data["enabled"] is True - + # Test update request serialization - update_request = (builder - .webhook_id("webhook-123") + update_request = ( + builder.webhook_id("webhook-123") .name("Updated Name") .enabled(False) - .build_webhook_update_request()) - + .build_webhook_update_request() + ) + # Verify serialization excludes webhook_id (goes in URL) - update_data = update_request.model_dump(exclude_none=True, exclude={"webhook_id"}) + update_data = update_request.model_dump( + exclude_none=True, exclude={"webhook_id"} + ) assert "webhook_id" not in update_data assert update_data["name"] == "Updated Name" assert update_data["enabled"] is False - @vcr.use_cassette("webhooks_comprehensive_workflow.yaml") + @vcr.use_cassette("webhooks_comprehensive_workflow.yaml") def test_comprehensive_webhooks_workflow(self, email_client, test_domain_id): """Test comprehensive workflow covering list, CRUD operations, error scenarios, and builder usage.""" # Test list with different configurations list_request = WebhooksListRequest( query_params=WebhooksListQueryParams(domain_id=test_domain_id) ) - + response = email_client.webhooks.list_webhooks(list_request) assert isinstance(response, APIResponse) assert response.status_code == 200 - + # Test builder pattern with different configurations builder = WebhooksBuilder() - + # Test list with builder - builder_request = builder.domain_id(test_domain_id).build_webhooks_list_request() + builder_request = builder.domain_id( + test_domain_id + ).build_webhooks_list_request() builder_response = email_client.webhooks.list_webhooks(builder_request) assert isinstance(builder_response, APIResponse) assert builder_response.status_code == 200 - + # Test error scenarios from mailersend.exceptions import ResourceNotFoundError - + # Test get non-existent webhook get_request = WebhookGetRequest(webhook_id="non-existent-id") with pytest.raises(ResourceNotFoundError): email_client.webhooks.get_webhook(get_request) - + # Test create webhook create_request = WebhookCreateRequest( url="https://example.com/webhook", name="Test Webhook", events=["activity.sent"], - domain_id=test_domain_id + domain_id=test_domain_id, ) try: create_response = email_client.webhooks.create_webhook(create_request) @@ -651,8 +684,10 @@ def test_comprehensive_webhooks_workflow(self, email_client, test_domain_id): except Exception: # Creation might fail due to quota limits or other reasons pass - + # Test builder error scenarios - builder_get_request = builder.webhook_id("another-non-existent-id").build_webhook_get_request() + builder_get_request = builder.webhook_id( + "another-non-existent-id" + ).build_webhook_get_request() with pytest.raises(ResourceNotFoundError): - email_client.webhooks.get_webhook(builder_get_request) \ No newline at end of file + email_client.webhooks.get_webhook(builder_get_request) diff --git a/tests/unit/test_analytics_builder.py b/tests/unit/test_analytics_builder.py index 35cc659..f3c58de 100644 --- a/tests/unit/test_analytics_builder.py +++ b/tests/unit/test_analytics_builder.py @@ -12,12 +12,7 @@ class TestAnalyticsBuilder: def test_basic_analytics_builder(self): """Test basic analytics builder construction""" - request = ( - AnalyticsBuilder() - .date_from(1443651141) - .date_to(1443661141) - .build() - ) + request = AnalyticsBuilder().date_from(1443651141).date_to(1443661141).build() assert isinstance(request, AnalyticsRequest) assert request.date_from == 1443651141 @@ -75,12 +70,7 @@ def test_recipient_limit_validation(self): def test_timestamp_date_methods(self): """Test timestamp-based date methods""" - request = ( - AnalyticsBuilder() - .date_from(1443651141) - .date_to(1443661141) - .build() - ) + request = AnalyticsBuilder().date_from(1443651141).date_to(1443661141).build() assert request.date_from == 1443651141 assert request.date_to == 1443661141 @@ -538,12 +528,7 @@ class TestAnalyticsBuilderEdgeCases: def test_empty_lists_in_build(self): """Test handling of empty lists in build""" - request = ( - AnalyticsBuilder() - .date_from(1443651141) - .date_to(1443661141) - .build() - ) + request = AnalyticsBuilder().date_from(1443651141).date_to(1443661141).build() # Empty lists should be None in the final request assert request.recipient_id is None @@ -568,17 +553,9 @@ def test_builder_state_isolation(self): builder1 = AnalyticsBuilder().domain_id("domain-1").tags("tag-1") builder2 = AnalyticsBuilder().domain_id("domain-2").tags("tag-2") - request1 = ( - builder1.date_from(1443651141) - .date_to(1443661141) - .build() - ) + request1 = builder1.date_from(1443651141).date_to(1443661141).build() - request2 = ( - builder2.date_from(1443651141) - .date_to(1443661141) - .build() - ) + request2 = builder2.date_from(1443651141).date_to(1443661141).build() assert request1.domain_id == "domain-1" assert request1.tags == ["tag-1"] diff --git a/tests/unit/test_analytics_resource.py b/tests/unit/test_analytics_resource.py index a09a325..e7aa540 100644 --- a/tests/unit/test_analytics_resource.py +++ b/tests/unit/test_analytics_resource.py @@ -240,7 +240,7 @@ def test_build_query_params_exclude_none_values(self): """Test that None values are excluded from query parameters""" request = AnalyticsRequest( date_from=1443651141, - date_to=1443661141 + date_to=1443661141, # domain_id is None, tags is None, etc. ) diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index c4ec786..c6a377a 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -12,84 +12,89 @@ class TestMailerSendClientInitialization: def test_client_initialization_with_explicit_api_key(self): """Test that client initializes correctly with explicit API key.""" - with patch('mailersend.client.get_logger'), \ - patch('mailersend.client.RequestLogger'), \ - patch('mailersend.client.requests.Session'): - + with patch("mailersend.client.get_logger"), patch( + "mailersend.client.RequestLogger" + ), patch("mailersend.client.requests.Session"): + client = MailerSendClient(api_key="test-api-key") assert client.api_key == "test-api-key" def test_client_initialization_with_env_var(self): """Test that client reads API key from environment variable.""" - with patch.dict(os.environ, {'MAILERSEND_API_KEY': 'env-api-key'}), \ - patch('mailersend.client.get_logger'), \ - patch('mailersend.client.RequestLogger'), \ - patch('mailersend.client.requests.Session'): - + with patch.dict(os.environ, {"MAILERSEND_API_KEY": "env-api-key"}), patch( + "mailersend.client.get_logger" + ), patch("mailersend.client.RequestLogger"), patch( + "mailersend.client.requests.Session" + ): + client = MailerSendClient() assert client.api_key == "env-api-key" def test_client_initialization_parameter_overrides_env(self): """Test that explicit API key parameter overrides environment variable.""" - with patch.dict(os.environ, {'MAILERSEND_API_KEY': 'env-api-key'}), \ - patch('mailersend.client.get_logger'), \ - patch('mailersend.client.RequestLogger'), \ - patch('mailersend.client.requests.Session'): - + with patch.dict(os.environ, {"MAILERSEND_API_KEY": "env-api-key"}), patch( + "mailersend.client.get_logger" + ), patch("mailersend.client.RequestLogger"), patch( + "mailersend.client.requests.Session" + ): + client = MailerSendClient(api_key="param-api-key") assert client.api_key == "param-api-key" def test_client_initialization_fails_without_api_key(self): """Test that client initialization fails when no API key is provided.""" - with patch.dict(os.environ, {}, clear=True), \ - patch('mailersend.client.get_logger'), \ - patch('mailersend.client.RequestLogger'), \ - patch('mailersend.client.requests.Session'): - + with patch.dict(os.environ, {}, clear=True), patch( + "mailersend.client.get_logger" + ), patch("mailersend.client.RequestLogger"), patch( + "mailersend.client.requests.Session" + ): + with pytest.raises(ValueError) as exc_info: MailerSendClient() - + assert "API key is required" in str(exc_info.value) def test_client_initialization_fails_with_none_api_key_and_no_env(self): """Test that client initialization fails with None API key and no env var.""" - with patch.dict(os.environ, {}, clear=True), \ - patch('mailersend.client.get_logger'), \ - patch('mailersend.client.RequestLogger'), \ - patch('mailersend.client.requests.Session'): - + with patch.dict(os.environ, {}, clear=True), patch( + "mailersend.client.get_logger" + ), patch("mailersend.client.RequestLogger"), patch( + "mailersend.client.requests.Session" + ): + with pytest.raises(ValueError) as exc_info: MailerSendClient(api_key=None) - + assert "API key is required" in str(exc_info.value) def test_client_initialization_with_empty_env_var(self): """Test that client handles empty environment variable correctly.""" - with patch.dict(os.environ, {'MAILERSEND_API_KEY': ''}), \ - patch('mailersend.client.get_logger'), \ - patch('mailersend.client.RequestLogger'), \ - patch('mailersend.client.requests.Session'): - + with patch.dict(os.environ, {"MAILERSEND_API_KEY": ""}), patch( + "mailersend.client.get_logger" + ), patch("mailersend.client.RequestLogger"), patch( + "mailersend.client.requests.Session" + ): + with pytest.raises(ValueError) as exc_info: MailerSendClient() - + assert "API key is required" in str(exc_info.value) def test_client_initialization_sets_other_properties(self): """Test that client initializes other properties correctly.""" - with patch('mailersend.client.get_logger'), \ - patch('mailersend.client.RequestLogger'), \ - patch('mailersend.client.requests.Session'): - + with patch("mailersend.client.get_logger"), patch( + "mailersend.client.RequestLogger" + ), patch("mailersend.client.requests.Session"): + client = MailerSendClient( api_key="test-key", base_url="https://custom.api.com", timeout=30, max_retries=5, - debug=True + debug=True, ) - + assert client.api_key == "test-key" assert client.base_url == "https://custom.api.com" assert client.timeout == 30 - assert client.debug is True \ No newline at end of file + assert client.debug is True diff --git a/tests/unit/test_dmarc_monitoring_resource.py b/tests/unit/test_dmarc_monitoring_resource.py new file mode 100644 index 0000000..a5125b7 --- /dev/null +++ b/tests/unit/test_dmarc_monitoring_resource.py @@ -0,0 +1,504 @@ +"""Unit tests for DMARC Monitoring resource.""" + +import pytest +from unittest.mock import Mock, MagicMock + +from mailersend.resources.dmarc_monitoring import DmarcMonitoring +from mailersend.models.base import APIResponse +from mailersend.models.dmarc_monitoring import ( + DmarcMonitoringListRequest, + DmarcMonitoringListQueryParams, + DmarcMonitoringCreateRequest, + DmarcMonitoringUpdateRequest, + DmarcMonitoringDeleteRequest, + DmarcMonitoringReportRequest, + DmarcMonitoringReportQueryParams, + DmarcMonitoringIpReportRequest, + DmarcMonitoringReportSourcesRequest, + DmarcMonitoringFavoriteRequest, +) + + +class TestDmarcMonitoringInit: + """Test DmarcMonitoring resource initialization.""" + + def test_initialization(self): + """Test DmarcMonitoring resource initializes correctly.""" + mock_client = Mock() + resource = DmarcMonitoring(mock_client) + + assert resource.client is mock_client + assert resource.logger is not None + + +class TestListMonitors: + """Test list_monitors method.""" + + def setup_method(self): + """Set up test fixtures.""" + self.mock_client = Mock() + self.resource = DmarcMonitoring(self.mock_client) + self.resource.logger = Mock() + self.mock_api_response = MagicMock(spec=APIResponse) + self.resource._create_response = Mock(return_value=self.mock_api_response) + + def test_list_monitors_returns_api_response(self): + """Test list_monitors returns APIResponse.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + result = self.resource.list_monitors() + + assert result == self.mock_api_response + self.resource._create_response.assert_called_once_with(mock_response) + + def test_list_monitors_default_params(self): + """Test list_monitors with no request uses defaults.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + self.resource.list_monitors() + + self.mock_client.request.assert_called_once_with( + method="GET", path="dmarc-monitoring", params={"page": 1, "limit": 25} + ) + + def test_list_monitors_with_custom_params(self): + """Test list_monitors with custom pagination.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + query_params = DmarcMonitoringListQueryParams(page=2, limit=50) + request = DmarcMonitoringListRequest(query_params=query_params) + + self.resource.list_monitors(request) + + self.mock_client.request.assert_called_once_with( + method="GET", path="dmarc-monitoring", params={"page": 2, "limit": 50} + ) + + def test_list_monitors_with_explicit_request(self): + """Test list_monitors with explicit request object.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + query_params = DmarcMonitoringListQueryParams(page=1, limit=10) + request = DmarcMonitoringListRequest(query_params=query_params) + + self.resource.list_monitors(request) + + self.mock_client.request.assert_called_once_with( + method="GET", path="dmarc-monitoring", params={"page": 1, "limit": 10} + ) + + +class TestCreateMonitor: + """Test create_monitor method.""" + + def setup_method(self): + """Set up test fixtures.""" + self.mock_client = Mock() + self.resource = DmarcMonitoring(self.mock_client) + self.resource.logger = Mock() + self.mock_api_response = MagicMock(spec=APIResponse) + self.resource._create_response = Mock(return_value=self.mock_api_response) + + def test_create_monitor_returns_api_response(self): + """Test create_monitor returns APIResponse.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + request = DmarcMonitoringCreateRequest(domain_id="domain-123") + result = self.resource.create_monitor(request) + + assert result == self.mock_api_response + self.resource._create_response.assert_called_once_with(mock_response) + + def test_create_monitor_sends_correct_body(self): + """Test create_monitor sends domain_id in request body.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + request = DmarcMonitoringCreateRequest(domain_id="domain-123") + self.resource.create_monitor(request) + + self.mock_client.request.assert_called_once_with( + method="POST", + path="dmarc-monitoring", + body={"domain_id": "domain-123"}, + ) + + def test_create_monitor_uses_post_method(self): + """Test create_monitor uses POST HTTP method.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + request = DmarcMonitoringCreateRequest(domain_id="domain-abc") + self.resource.create_monitor(request) + + call_args = self.mock_client.request.call_args + assert call_args[1]["method"] == "POST" + + +class TestUpdateMonitor: + """Test update_monitor method.""" + + def setup_method(self): + """Set up test fixtures.""" + self.mock_client = Mock() + self.resource = DmarcMonitoring(self.mock_client) + self.resource.logger = Mock() + self.mock_api_response = MagicMock(spec=APIResponse) + self.resource._create_response = Mock(return_value=self.mock_api_response) + + def test_update_monitor_returns_api_response(self): + """Test update_monitor returns APIResponse.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + request = DmarcMonitoringUpdateRequest( + monitor_id="monitor-123", + wanted_dmarc_record="v=DMARC1; p=reject;", + ) + result = self.resource.update_monitor(request) + + assert result == self.mock_api_response + self.resource._create_response.assert_called_once_with(mock_response) + + def test_update_monitor_sends_correct_body_and_path(self): + """Test update_monitor sends wanted_dmarc_record in body and monitor_id in path.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + request = DmarcMonitoringUpdateRequest( + monitor_id="monitor-123", + wanted_dmarc_record="v=DMARC1; p=reject;", + ) + self.resource.update_monitor(request) + + self.mock_client.request.assert_called_once_with( + method="PUT", + path="dmarc-monitoring/monitor-123", + body={"wanted_dmarc_record": "v=DMARC1; p=reject;"}, + ) + + def test_update_monitor_excludes_monitor_id_from_body(self): + """Test update_monitor does not include monitor_id in request body.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + request = DmarcMonitoringUpdateRequest( + monitor_id="monitor-123", + wanted_dmarc_record="v=DMARC1; p=none;", + ) + self.resource.update_monitor(request) + + call_args = self.mock_client.request.call_args + body = call_args[1]["body"] + assert "monitor_id" not in body + assert "wanted_dmarc_record" in body + + +class TestDeleteMonitor: + """Test delete_monitor method.""" + + def setup_method(self): + """Set up test fixtures.""" + self.mock_client = Mock() + self.resource = DmarcMonitoring(self.mock_client) + self.resource.logger = Mock() + self.mock_api_response = MagicMock(spec=APIResponse) + self.resource._create_response = Mock(return_value=self.mock_api_response) + + def test_delete_monitor_returns_api_response(self): + """Test delete_monitor returns APIResponse.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + request = DmarcMonitoringDeleteRequest(monitor_id="monitor-123") + result = self.resource.delete_monitor(request) + + assert result == self.mock_api_response + self.resource._create_response.assert_called_once_with(mock_response) + + def test_delete_monitor_uses_correct_path(self): + """Test delete_monitor constructs correct endpoint path.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + request = DmarcMonitoringDeleteRequest(monitor_id="monitor-abc") + self.resource.delete_monitor(request) + + self.mock_client.request.assert_called_once_with( + method="DELETE", path="dmarc-monitoring/monitor-abc" + ) + + +class TestGetAggregatedReport: + """Test get_aggregated_report method.""" + + def setup_method(self): + """Set up test fixtures.""" + self.mock_client = Mock() + self.resource = DmarcMonitoring(self.mock_client) + self.resource.logger = Mock() + self.mock_api_response = MagicMock(spec=APIResponse) + self.resource._create_response = Mock(return_value=self.mock_api_response) + + def test_get_aggregated_report_returns_api_response(self): + """Test get_aggregated_report returns APIResponse.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + request = DmarcMonitoringReportRequest(monitor_id="monitor-123") + result = self.resource.get_aggregated_report(request) + + assert result == self.mock_api_response + self.resource._create_response.assert_called_once_with(mock_response) + + def test_get_aggregated_report_default_params(self): + """Test get_aggregated_report with default pagination.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + request = DmarcMonitoringReportRequest(monitor_id="monitor-123") + self.resource.get_aggregated_report(request) + + self.mock_client.request.assert_called_once_with( + method="GET", + path="dmarc-monitoring/monitor-123/report", + params={"page": 1, "limit": 25}, + ) + + def test_get_aggregated_report_custom_params(self): + """Test get_aggregated_report with custom pagination.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + query_params = DmarcMonitoringReportQueryParams(page=3, limit=100) + request = DmarcMonitoringReportRequest( + monitor_id="monitor-123", query_params=query_params + ) + self.resource.get_aggregated_report(request) + + self.mock_client.request.assert_called_once_with( + method="GET", + path="dmarc-monitoring/monitor-123/report", + params={"page": 3, "limit": 100}, + ) + + +class TestGetIpReport: + """Test get_ip_report method.""" + + def setup_method(self): + """Set up test fixtures.""" + self.mock_client = Mock() + self.resource = DmarcMonitoring(self.mock_client) + self.resource.logger = Mock() + self.mock_api_response = MagicMock(spec=APIResponse) + self.resource._create_response = Mock(return_value=self.mock_api_response) + + def test_get_ip_report_returns_api_response(self): + """Test get_ip_report returns APIResponse.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + request = DmarcMonitoringIpReportRequest( + monitor_id="monitor-123", ip="192.168.1.1" + ) + result = self.resource.get_ip_report(request) + + assert result == self.mock_api_response + self.resource._create_response.assert_called_once_with(mock_response) + + def test_get_ip_report_constructs_correct_path(self): + """Test get_ip_report constructs correct endpoint path.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + request = DmarcMonitoringIpReportRequest( + monitor_id="monitor-123", ip="10.0.0.1" + ) + self.resource.get_ip_report(request) + + self.mock_client.request.assert_called_once_with( + method="GET", + path="dmarc-monitoring/monitor-123/report/10.0.0.1", + params={"page": 1, "limit": 25}, + ) + + def test_get_ip_report_with_custom_params(self): + """Test get_ip_report with custom pagination.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + query_params = DmarcMonitoringReportQueryParams(page=2, limit=50) + request = DmarcMonitoringIpReportRequest( + monitor_id="monitor-123", ip="10.0.0.1", query_params=query_params + ) + self.resource.get_ip_report(request) + + self.mock_client.request.assert_called_once_with( + method="GET", + path="dmarc-monitoring/monitor-123/report/10.0.0.1", + params={"page": 2, "limit": 50}, + ) + + +class TestGetReportSources: + """Test get_report_sources method.""" + + def setup_method(self): + """Set up test fixtures.""" + self.mock_client = Mock() + self.resource = DmarcMonitoring(self.mock_client) + self.resource.logger = Mock() + self.mock_api_response = MagicMock(spec=APIResponse) + self.resource._create_response = Mock(return_value=self.mock_api_response) + + def test_get_report_sources_returns_api_response(self): + """Test get_report_sources returns APIResponse.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + request = DmarcMonitoringReportSourcesRequest(monitor_id="monitor-123") + result = self.resource.get_report_sources(request) + + assert result == self.mock_api_response + self.resource._create_response.assert_called_once_with(mock_response) + + def test_get_report_sources_constructs_correct_path(self): + """Test get_report_sources constructs correct endpoint path.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + request = DmarcMonitoringReportSourcesRequest(monitor_id="monitor-abc") + self.resource.get_report_sources(request) + + self.mock_client.request.assert_called_once_with( + method="GET", + path="dmarc-monitoring/monitor-abc/report-sources", + ) + + +class TestMarkIpFavorite: + """Test mark_ip_favorite method.""" + + def setup_method(self): + """Set up test fixtures.""" + self.mock_client = Mock() + self.resource = DmarcMonitoring(self.mock_client) + self.resource.logger = Mock() + self.mock_api_response = MagicMock(spec=APIResponse) + self.resource._create_response = Mock(return_value=self.mock_api_response) + + def test_mark_ip_favorite_returns_api_response(self): + """Test mark_ip_favorite returns APIResponse.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + request = DmarcMonitoringFavoriteRequest( + monitor_id="monitor-123", ip="192.168.1.1" + ) + result = self.resource.mark_ip_favorite(request) + + assert result == self.mock_api_response + self.resource._create_response.assert_called_once_with(mock_response) + + def test_mark_ip_favorite_constructs_correct_path(self): + """Test mark_ip_favorite constructs correct endpoint path.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + request = DmarcMonitoringFavoriteRequest( + monitor_id="monitor-123", ip="10.0.0.1" + ) + self.resource.mark_ip_favorite(request) + + self.mock_client.request.assert_called_once_with( + method="PUT", + path="dmarc-monitoring/monitor-123/favorite/10.0.0.1", + ) + + +class TestRemoveIpFavorite: + """Test remove_ip_favorite method.""" + + def setup_method(self): + """Set up test fixtures.""" + self.mock_client = Mock() + self.resource = DmarcMonitoring(self.mock_client) + self.resource.logger = Mock() + self.mock_api_response = MagicMock(spec=APIResponse) + self.resource._create_response = Mock(return_value=self.mock_api_response) + + def test_remove_ip_favorite_returns_api_response(self): + """Test remove_ip_favorite returns APIResponse.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + request = DmarcMonitoringFavoriteRequest( + monitor_id="monitor-123", ip="192.168.1.1" + ) + result = self.resource.remove_ip_favorite(request) + + assert result == self.mock_api_response + self.resource._create_response.assert_called_once_with(mock_response) + + def test_remove_ip_favorite_constructs_correct_path(self): + """Test remove_ip_favorite constructs correct endpoint path.""" + mock_response = Mock() + self.mock_client.request.return_value = mock_response + + request = DmarcMonitoringFavoriteRequest( + monitor_id="monitor-123", ip="10.0.0.1" + ) + self.resource.remove_ip_favorite(request) + + self.mock_client.request.assert_called_once_with( + method="DELETE", + path="dmarc-monitoring/monitor-123/favorite/10.0.0.1", + ) + + +class TestModelValidation: + """Test model validation.""" + + def test_create_request_requires_domain_id(self): + """Test DmarcMonitoringCreateRequest raises error for empty domain_id.""" + with pytest.raises(Exception): + DmarcMonitoringCreateRequest(domain_id="") + + def test_update_request_requires_monitor_id(self): + """Test DmarcMonitoringUpdateRequest raises error for empty monitor_id.""" + with pytest.raises(Exception): + DmarcMonitoringUpdateRequest( + monitor_id="", wanted_dmarc_record="v=DMARC1; p=reject;" + ) + + def test_update_request_requires_wanted_dmarc_record(self): + """Test DmarcMonitoringUpdateRequest raises error for empty wanted_dmarc_record.""" + with pytest.raises(Exception): + DmarcMonitoringUpdateRequest( + monitor_id="monitor-123", wanted_dmarc_record="" + ) + + def test_list_query_params_default_values(self): + """Test DmarcMonitoringListQueryParams has correct defaults.""" + params = DmarcMonitoringListQueryParams() + assert params.page == 1 + assert params.limit == 25 + + def test_list_query_params_limit_validation(self): + """Test DmarcMonitoringListQueryParams validates limit range.""" + with pytest.raises(Exception): + DmarcMonitoringListQueryParams(limit=5) # below min of 10 + + with pytest.raises(Exception): + DmarcMonitoringListQueryParams(limit=101) # above max of 100 + + def test_favorite_request_requires_ip(self): + """Test DmarcMonitoringFavoriteRequest raises error for empty ip.""" + with pytest.raises(Exception): + DmarcMonitoringFavoriteRequest(monitor_id="monitor-123", ip="") diff --git a/tests/unit/test_domains_resource.py b/tests/unit/test_domains_resource.py index b442c71..0c2b180 100644 --- a/tests/unit/test_domains_resource.py +++ b/tests/unit/test_domains_resource.py @@ -91,7 +91,9 @@ def test_recipients_query_params_delegation(self): expected_params = {"page": 3, "limit": 20} mock_client.request.assert_called_once_with( - method="GET", path="domains/test-domain-id/recipients", params=expected_params + method="GET", + path="domains/test-domain-id/recipients", + params=expected_params, ) domains._create_response.assert_called_once_with(mock_response) assert isinstance(result, type(domains._create_response.return_value)) diff --git a/tests/unit/test_email_builder.py b/tests/unit/test_email_builder.py index 39e002e..cbda04d 100644 --- a/tests/unit/test_email_builder.py +++ b/tests/unit/test_email_builder.py @@ -194,10 +194,12 @@ def test_cc_bcc_mixed_usage(self): .cc("single-cc@example.com", "Single CC") .cc([{"email": "array-cc1@example.com", "name": "Array CC 1"}]) .bcc("single-bcc@example.com") - .bcc_many([ - {"email": "many-bcc1@example.com", "name": "Many BCC 1"}, - {"email": "many-bcc2@example.com"} - ]) + .bcc_many( + [ + {"email": "many-bcc1@example.com", "name": "Many BCC 1"}, + {"email": "many-bcc2@example.com"}, + ] + ) .subject("Mixed Usage Test") .text("Test message") .build() @@ -222,11 +224,15 @@ def test_cc_bcc_invalid_input_validation(self): builder = EmailBuilder() # Test invalid type for cc method - with pytest.raises(ValidationError, match="Email must be a string or list of recipient objects"): + with pytest.raises( + ValidationError, match="Email must be a string or list of recipient objects" + ): builder.cc(123) # Invalid type # Test invalid type for bcc method - with pytest.raises(ValidationError, match="Email must be a string or list of recipient objects"): + with pytest.raises( + ValidationError, match="Email must be a string or list of recipient objects" + ): builder.bcc({"email": "test@example.com"}) # Dict instead of string or list def test_content_methods(self): diff --git a/tests/unit/test_email_verification_resource.py b/tests/unit/test_email_verification_resource.py index df34bbd..f71f5c9 100644 --- a/tests/unit/test_email_verification_resource.py +++ b/tests/unit/test_email_verification_resource.py @@ -38,7 +38,9 @@ def test_verify_email_valid_request(self): assert isinstance(result, APIResponse) self.mock_client.request.assert_called_once_with( - method="POST", path="email-verification/verify", body={"email": "test@example.com"} + method="POST", + path="email-verification/verify", + body={"email": "test@example.com"}, ) def test_verify_email_async_valid_request(self): @@ -199,7 +201,9 @@ def test_create_verification_valid_request(self): assert isinstance(result, APIResponse) self.mock_client.request.assert_called_once_with( - method="POST", path="email-verification", body={"name": "Test List", "emails": emails} + method="POST", + path="email-verification", + body={"name": "Test List", "emails": emails}, ) def test_verify_list_valid_request(self): diff --git a/tests/unit/test_inbound_builder.py b/tests/unit/test_inbound_builder.py index 5d25e58..2a72582 100644 --- a/tests/unit/test_inbound_builder.py +++ b/tests/unit/test_inbound_builder.py @@ -274,9 +274,13 @@ def test_build_create_request(self): assert request.name == "Test Route" assert request.domain_enabled is False assert request.catch_filter.type == "catch_all" - assert request.catch_filter.filters is None # catch_all doesn't need specific filters + assert ( + request.catch_filter.filters is None + ) # catch_all doesn't need specific filters assert request.match_filter.type == "match_all" - assert request.match_filter.filters is None # match_all doesn't need specific filters + assert ( + request.match_filter.filters is None + ) # match_all doesn't need specific filters assert len(request.forwards) == 1 def test_build_create_request_with_domain_enabled(self): diff --git a/tests/unit/test_recipients_builder.py b/tests/unit/test_recipients_builder.py index be04b00..786c61a 100644 --- a/tests/unit/test_recipients_builder.py +++ b/tests/unit/test_recipients_builder.py @@ -1,4 +1,5 @@ """Test cases for Recipients API builder.""" + import pytest from mailersend.builders.recipients import RecipientsBuilder diff --git a/tests/unit/test_recipients_models.py b/tests/unit/test_recipients_models.py index 74c65d5..af5720a 100644 --- a/tests/unit/test_recipients_models.py +++ b/tests/unit/test_recipients_models.py @@ -1,4 +1,5 @@ """Unit tests for Recipients models.""" + import pytest from datetime import datetime from pydantic import ValidationError diff --git a/tests/unit/test_recipients_resource.py b/tests/unit/test_recipients_resource.py index f4bd096..6553051 100644 --- a/tests/unit/test_recipients_resource.py +++ b/tests/unit/test_recipients_resource.py @@ -1,4 +1,5 @@ """Unit tests for Recipients resource.""" + import pytest from unittest.mock import Mock, MagicMock from requests import Response diff --git a/tests/unit/test_schedules_resource.py b/tests/unit/test_schedules_resource.py index ee3c968..3948150 100644 --- a/tests/unit/test_schedules_resource.py +++ b/tests/unit/test_schedules_resource.py @@ -1,4 +1,5 @@ """Unit tests for Schedules resource.""" + import pytest from unittest.mock import Mock, MagicMock diff --git a/tests/unit/test_sms_activity_resource.py b/tests/unit/test_sms_activity_resource.py index 5634c3e..940266d 100644 --- a/tests/unit/test_sms_activity_resource.py +++ b/tests/unit/test_sms_activity_resource.py @@ -1,4 +1,5 @@ """Unit tests for SMS Activity resource.""" + import pytest from unittest.mock import Mock, MagicMock diff --git a/tests/unit/test_sms_inbounds_resource.py b/tests/unit/test_sms_inbounds_resource.py index 87bea82..b8f67a2 100644 --- a/tests/unit/test_sms_inbounds_resource.py +++ b/tests/unit/test_sms_inbounds_resource.py @@ -1,4 +1,5 @@ """Unit tests for SMS Inbounds resource.""" + from unittest.mock import Mock, MagicMock from mailersend.resources.sms_inbounds import SmsInbounds diff --git a/tests/unit/test_sms_messages_resource.py b/tests/unit/test_sms_messages_resource.py index 4a28e2f..3066860 100644 --- a/tests/unit/test_sms_messages_resource.py +++ b/tests/unit/test_sms_messages_resource.py @@ -1,4 +1,5 @@ """Unit tests for SMS Messages resource.""" + import pytest from unittest.mock import Mock, MagicMock diff --git a/tests/unit/test_sms_numbers_resource.py b/tests/unit/test_sms_numbers_resource.py index 2fcdd0c..4652813 100644 --- a/tests/unit/test_sms_numbers_resource.py +++ b/tests/unit/test_sms_numbers_resource.py @@ -1,4 +1,5 @@ """Unit tests for SMS Numbers resource.""" + from unittest.mock import Mock, MagicMock from mailersend.resources.sms_numbers import SmsNumbers diff --git a/tests/unit/test_sms_recipients_resource.py b/tests/unit/test_sms_recipients_resource.py index 291a1d6..3455c1c 100644 --- a/tests/unit/test_sms_recipients_resource.py +++ b/tests/unit/test_sms_recipients_resource.py @@ -1,4 +1,5 @@ """Unit tests for SMS Recipients resource.""" + from unittest.mock import Mock, MagicMock from mailersend.resources.sms_recipients import SmsRecipients diff --git a/tests/unit/test_sms_sending_resource.py b/tests/unit/test_sms_sending_resource.py index 7e1f36f..4443fa6 100644 --- a/tests/unit/test_sms_sending_resource.py +++ b/tests/unit/test_sms_sending_resource.py @@ -1,4 +1,5 @@ """Unit tests for SMS Sending resource.""" + import pytest from unittest.mock import Mock, MagicMock diff --git a/tests/unit/test_sms_webhooks_resource.py b/tests/unit/test_sms_webhooks_resource.py index eecb387..13eb686 100644 --- a/tests/unit/test_sms_webhooks_resource.py +++ b/tests/unit/test_sms_webhooks_resource.py @@ -1,4 +1,5 @@ """Unit tests for SMS Webhooks resource.""" + import pytest from unittest.mock import Mock, MagicMock diff --git a/tests/unit/test_smtp_users_resource.py b/tests/unit/test_smtp_users_resource.py index 9afd94a..c95c334 100644 --- a/tests/unit/test_smtp_users_resource.py +++ b/tests/unit/test_smtp_users_resource.py @@ -1,4 +1,5 @@ """Unit tests for SMTP Users resource.""" + from unittest.mock import Mock, MagicMock from mailersend.resources.smtp_users import SmtpUsers diff --git a/tests/unit/test_templates_builder.py b/tests/unit/test_templates_builder.py index ae216a8..643c03e 100644 --- a/tests/unit/test_templates_builder.py +++ b/tests/unit/test_templates_builder.py @@ -1,4 +1,5 @@ """Tests for Templates builder.""" + import pytest from mailersend.builders.templates import TemplatesBuilder diff --git a/tests/unit/test_templates_resource.py b/tests/unit/test_templates_resource.py index 682c5a3..6237eba 100644 --- a/tests/unit/test_templates_resource.py +++ b/tests/unit/test_templates_resource.py @@ -1,4 +1,5 @@ """Unit tests for Templates resource.""" + import pytest from unittest.mock import Mock, MagicMock diff --git a/tests/unit/test_tokens_resource.py b/tests/unit/test_tokens_resource.py index 8791fcf..0e92a6f 100644 --- a/tests/unit/test_tokens_resource.py +++ b/tests/unit/test_tokens_resource.py @@ -1,4 +1,5 @@ """Unit tests for Tokens resource.""" + import pytest from unittest.mock import Mock, MagicMock diff --git a/tests/unit/test_users_resource.py b/tests/unit/test_users_resource.py index 4b979d0..2b2f93c 100644 --- a/tests/unit/test_users_resource.py +++ b/tests/unit/test_users_resource.py @@ -1,4 +1,5 @@ """Unit tests for Users resource.""" + import pytest from unittest.mock import Mock, MagicMock diff --git a/tests/unit/test_webhooks_resource.py b/tests/unit/test_webhooks_resource.py index 7cd386e..ee75439 100644 --- a/tests/unit/test_webhooks_resource.py +++ b/tests/unit/test_webhooks_resource.py @@ -1,4 +1,5 @@ """Unit tests for Webhooks resource.""" + from unittest.mock import Mock, MagicMock from mailersend.resources.webhooks import Webhooks