From dcc75812bb0cc80de20dc08e35139231101f55e8 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 6 Nov 2024 10:24:05 +0200 Subject: [PATCH 1/8] docs: Add docstrings for classes, mehods and functions (following PEP 257 Docstring Conventions); linting --- mailjet_rest/client.py | 335 +++++++++++++++++++++++++++++++++++++++-- tests/test_client.py | 162 ++++++++++++++++++++ 2 files changed, 486 insertions(+), 11 deletions(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 26186bb..e3e4a5a 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -23,7 +23,15 @@ def prepare_url(key: Match[str]) -> str: - """Replaces capital letters to lower one with dash prefix.""" + """ + Replaces capital letters in the input string with a dash prefix and converts them to lowercase. + + Parameters: + key (Match[str]): A match object representing a substring from the input string. The substring should contain a single capital letter. + + Returns: + str: A string containing a dash followed by the lowercase version of the input capital letter. + """ char_elem = key.group(0) if char_elem.isupper(): return "-" + char_elem.lower() @@ -31,17 +39,64 @@ def prepare_url(key: Match[str]) -> str: class Config: + """ + Configuration settings for interacting with the Mailjet API. + + This class stores and manages API configuration details, including the API URL, + version, and user agent string. It provides methods for initializing these settings + and generating endpoint-specific URLs and headers as required for API interactions. + + Attributes: + DEFAULT_API_URL (str): The default base URL for Mailjet API requests. + API_REF (str): Reference URL for Mailjet's API documentation. + version (str): API version to use, defaulting to 'v3'. + user_agent (str): User agent string including the package version for tracking. + """ + DEFAULT_API_URL: str = "https://api.mailjet.com/" - API_REF: str = "http://dev.mailjet.com/email-api/v3/" + API_REF: str = "https://dev.mailjet.com/email-api/v3/" version: str = "v3" user_agent: str = "mailjet-apiv3-python/v" + get_version() def __init__(self, version: str | None = None, api_url: str | None = None) -> None: + """ + Initialize a new Config instance with specified or default API settings. + + This initializer sets the API version and base URL. If no version or URL + is provided, it defaults to the predefined class values. + + Parameters: + - version (str | None): The API version to use. If None, the default version ('v3') is used. + - api_url (str | None): The base URL for API requests. If None, the default URL (DEFAULT_API_URL) is used. + + Returns: + - None + """ if version is not None: self.version = version self.api_url = api_url or self.DEFAULT_API_URL def __getitem__(self, key: str) -> tuple[str, dict[str, str]]: + """ + Retrieve the API endpoint URL and headers for a given key. + + This method builds the URL and headers required for specific API interactions. + The URL is adjusted based on the API version, and additional headers are + appended depending on the endpoint type. Specific keys modify content-type + for endpoints expecting CSV or plain text. + + Parameters: + - key (str): The name of the API endpoint, which influences URL structure and header configuration. + + Returns: + - tuple[str, dict[str, str]]: A tuple containing the constructed URL and headers required for the specified endpoint. + + Examples: + For the "contactslist_csvdata" key, a URL pointing to 'DATA/' and a + 'Content-type' of 'text/plain' is returned. + For the "batchjob_csverror" key, a URL with 'DATA/' and a 'Content-type' + of 'text/csv' is returned. + """ # Append version to URL. # Forward slash is ignored if present in self.version. url = urljoin(self.api_url, self.version + "/") @@ -62,6 +117,28 @@ def __getitem__(self, key: str) -> tuple[str, dict[str, str]]: class Endpoint: + """ + A class representing a specific Mailjet API endpoint. + + This class provides methods to perform HTTP requests to a given API endpoint, + including GET, POST, PUT, and DELETE requests. It manages URL construction, + headers, and authentication for interacting with the endpoint. + + Attributes: + - _url (str): The base URL of the endpoint. + - headers (dict[str, str]): The headers to be included in API requests. + - _auth (tuple[str, str] | None): The authentication credentials. + - action (str | None): The specific action to be performed on the endpoint. + + Methods: + - _get: Internal method to perform a GET request. + - get_many: Performs a GET request to retrieve multiple resources. + - get: Performs a GET request to retrieve a specific resource. + - create: Performs a POST request to create a new resource. + - update: Performs a PUT request to update an existing resource. + - delete: Performs a DELETE request to delete a resource. + """ + def __init__( self, url: str, @@ -69,6 +146,15 @@ def __init__( auth: tuple[str, str] | None, action: str | None = None, ) -> None: + """ + Initialize a new Endpoint instance. + + Args: + url (str): The base URL for the endpoint. + headers (dict[str, str]): Headers for API requests. + auth (tuple[str, str] | None): Authentication credentials. + action (str | None): Action to perform on the endpoint, if any. + """ self._url, self.headers, self._auth, self.action = url, headers, auth, action def _get( @@ -78,6 +164,21 @@ def _get( id: str | None = None, **kwargs: Any, ) -> Response: + """ + Perform an internal GET request to the endpoint. + + Constructs the URL with the provided filters and action_id to retrieve + specific data from the API. + + Parameters: + - filters (Mapping[str, str | Any] | None): Filters to be applied in the request. + - action_id (str | None): The specific action ID for the endpoint to be performed. + - id (str | None): The ID of the specific resource to be retrieved. + - **kwargs (Any): Additional keyword arguments to be passed to the API call. + + Returns: + - Response: The response object from the API call. + """ return api_call( self._auth, "get", @@ -96,6 +197,17 @@ def get_many( action_id: str | None = None, **kwargs: Any, ) -> Response: + """ + Performs a GET request to retrieve multiple resources. + + Parameters: + - filters (Mapping[str, str | Any] | None): Filters to be applied in the request. + - action_id (str | None): The specific action ID to be performed. + - **kwargs (Any): Additional keyword arguments to be passed to the API call. + + Returns: + - Response: The response object from the API call containing multiple resources. + """ return self._get(filters=filters, action_id=action_id, **kwargs) def get( @@ -105,6 +217,18 @@ def get( action_id: str | None = None, **kwargs: Any, ) -> Response: + """ + Performs a GET request to retrieve a specific resource. + + Parameters: + - id (str | None): The ID of the specific resource to be retrieved. + - filters (Mapping[str, str | Any] | None): Filters to be applied in the request. + - action_id (str | None): The specific action ID to be performed. + - **kwargs (Any): Additional keyword arguments to be passed to the API call. + + Returns: + - Response: The response object from the API call containing the specific resource. + """ return self._get(id=id, filters=filters, action_id=action_id, **kwargs) def create( @@ -117,6 +241,21 @@ def create( data_encoding: str = "utf-8", **kwargs: Any, ) -> Response: + """ + Performs a POST request to create a new resource. + + Parameters: + - data (dict | None): The data to include in the request body. + - filters (Mapping[str, str | Any] | None): Filters to be applied in the request. + - id (str | None): The ID of the specific resource to be created. + - action_id (str | None): The specific action ID to be performed. + - ensure_ascii (bool): Whether to ensure ASCII characters in the data. + - data_encoding (str): The encoding to be used for the data. + - **kwargs (Any): Additional keyword arguments to be passed to the API call. + + Returns: + - Response: The response object from the API call. + """ json_data: str | bytes | None = None if self.headers.get("Content-type") == "application/json" and data is not None: json_data = json.dumps(data, ensure_ascii=ensure_ascii) @@ -145,6 +284,21 @@ def update( data_encoding: str = "utf-8", **kwargs: Any, ) -> Response: + """ + Performs a PUT request to update an existing resource. + + Parameters: + - id (str | None): The ID of the specific resource to be updated. + - data (dict | None): The data to be sent in the request body. + - filters (Mapping[str, str | Any] | None): Filters to be applied in the request. + - action_id (str | None): The specific action ID to be performed. + - ensure_ascii (bool): Whether to ensure ASCII characters in the data. + - data_encoding (str): The encoding to be used for the data. + - **kwargs (Any): Additional keyword arguments to be passed to the API call. + + Returns: + - Response: The response object from the API call. + """ json_data: str | bytes | None = None if self.headers.get("Content-type") == "application/json" and data is not None: json_data = json.dumps(data, ensure_ascii=ensure_ascii) @@ -164,6 +318,16 @@ def update( ) def delete(self, id: str | None, **kwargs: Any) -> Response: + """ + Performs a DELETE request to delete a resource. + + Parameters: + - id (str | None): The ID of the specific resource to be deleted. + - **kwargs (Any): Additional keyword arguments to be passed to the API call. + + Returns: + - Response: The response object from the API call. + """ return api_call( self._auth, "delete", @@ -176,13 +340,61 @@ def delete(self, id: str | None, **kwargs: Any) -> Response: class Client: + """ + A client for interacting with the Mailjet API. + + This class manages authentication, configuration, and API endpoint access. + It initializes with API authentication details and uses dynamic attribute access + to allow flexible interaction with various Mailjet API endpoints. + + Attributes: + - auth (tuple[str, str] | None): A tuple containing the API key and secret for authentication. + - config (Config): An instance of the Config class, which holds API configuration settings. + + Methods: + - __init__: Initializes a new Client instance with authentication and configuration settings. + - __getattr__: Handles dynamic attribute access, allowing for accessing API endpoints as attributes. + """ + def __init__(self, auth: tuple[str, str] | None = None, **kwargs: Any) -> None: + """ + Initialize a new Client instance for API interaction. + + This method sets up API authentication and configuration. The `auth` parameter + provides a tuple with the API key and secret. Additional keyword arguments can + specify configuration options like API version and URL. + + Parameters: + - auth (tuple[str, str] | None): A tuple containing the API key and secret for authentication. If None, authentication is not required. + - **kwargs (Any): Additional keyword arguments, such as `version` and `api_url`, for configuring the client. + + Returns: + - None + + + Example: + client = Client(auth=("api_key", "api_secret"), version="v3") + """ self.auth = auth version: str | None = kwargs.get("version") api_url: str | None = kwargs.get("api_url") self.config = Config(version=version, api_url=api_url) def __getattr__(self, name: str) -> Any: + """ + Dynamically access API endpoints as attributes. + + This method allows for flexible, attribute-style access to API endpoints. + It constructs the appropriate endpoint URL and headers based on the attribute + name, which it parses to identify the resource and optional sub-resources. + + Parameters: + - name (str): The name of the attribute being accessed, corresponding to the Mailjet API endpoint. + + + Returns: + - Endpoint: An instance of the `Endpoint` class, initialized with the constructed URL, headers, action, and authentication details. + """ name_regex: str = re.sub(r"[A-Z]", prepare_url, name) split: list[str] = name_regex.split("_") # noqa: RUF100, FURB184 # identify the resource @@ -218,6 +430,26 @@ def api_call( action_id: str | None = None, **kwargs: Any, ) -> Response | Any: + """ + This function is responsible for making an API call to a specified URL using the provided method, headers, and other parameters. + + Parameters: + - auth (tuple[str, str] | None): A tuple containing the API key and secret for authentication. + - method (str): The HTTP method to be used for the API call (e.g., 'get', 'post', 'put', 'delete'). + - url (str): The URL to which the API call will be made. + - headers (dict[str, str]): A dictionary containing the headers to be included in the API call. + - data (str | bytes | None): The data to be sent in the request body. + - filters (Mapping[str, str | Any] | None): A dictionary containing filters to be applied in the request. + - resource_id (str | None): The ID of the specific resource to be accessed. + - timeout (int): The timeout for the API call in seconds. + - debug (bool): A flag indicating whether debug mode is enabled. + - action (str | None): The specific action to be performed on the resource. + - action_id (str | None): The ID of the specific action to be performed. + - **kwargs (Any): Additional keyword arguments to be passed to the API call. + + Returns: + - Response | Any: The response object from the API call if the request is successful, or an exception if an error occurs. + """ url = build_url( url, method=method, @@ -257,7 +489,17 @@ def build_headers( action: str, extra_headers: dict[str, str] | None = None, ) -> dict[str, str]: - """Build headers based on resource and action.""" + """ + Build headers based on resource and action. + + Parameters: + - resource (str): The name of the resource for which headers are being built. + - action (str): The specific action being performed on the resource. + - extra_headers (dict[str, str] | None): Additional headers to be included in the request. Defaults to None. + + Returns: + - dict[str, str]: A dictionary containing the headers to be included in the API request. + """ headers: dict[str, str] = {"Content-type": "application/json"} if resource.lower() == "contactslist" and action.lower() == "csvdata": @@ -278,6 +520,22 @@ def build_url( resource_id: str | None = None, action_id: str | None = None, ) -> str: + """ + Construct a URL for making an API request. + + This function takes the base URL, method, action, resource ID, and action ID as parameters + and constructs a URL by appending the resource ID, action, and action ID to the base URL. + + Parameters: + url (str): The base URL for the API request. + method (str | None): The HTTP method for the API request (e.g., 'get', 'post', 'put', 'delete'). + action (str | None): The specific action to be performed on the resource. Defaults to None. + resource_id (str | None): The ID of the specific resource to be accessed. Defaults to None. + action_id (str | None): The ID of the specific action to be performed. Defaults to None. + + Returns: + str: The constructed URL for the API request. + """ if resource_id: url += f"/{resource_id}" if action: @@ -288,6 +546,18 @@ def build_url( def parse_response(response: Response, debug: bool = False) -> Any: + """ + Parse the response from an API request. + + This function extracts the JSON data from the response and logs debug information if the `debug` flag is set to True. + + Parameters: + response (requests.models.Response): The response object from the API request. + debug (bool, optional): A flag indicating whether debug information should be logged. Defaults to False. + + Returns: + Any: The JSON data extracted from the response. + """ data = response.json() if debug: @@ -303,32 +573,75 @@ def parse_response(response: Response, debug: bool = False) -> Any: class ApiError(Exception): - pass + """ + Base class for all API-related errors. + + This exception serves as the root for all custom API error types, + allowing for more specific error handling based on the type of API + failure encountered. + """ class AuthorizationError(ApiError): - pass + """ + Error raised for authorization failures. + + This error is raised when the API request fails due to invalid + or missing authentication credentials. + """ class ActionDeniedError(ApiError): - pass + """ + Error raised when an action is denied by the API. + + This exception is triggered when an action is requested but is not + permitted, likely due to insufficient permissions. + """ class CriticalApiError(ApiError): - pass + """ + Error raised for critical API failures. + + This error represents severe issues with the API or infrastructure + that prevent requests from completing. + """ class ApiRateLimitError(ApiError): - pass + """ + Error raised when the API rate limit is exceeded. + + This exception is raised when the user has made too many requests + within a given time frame, as enforced by the API's rate limit policy. + """ class TimeoutError(ApiError): - pass + """ + Error raised when an API request times out. + + This error is raised if an API request does not complete within + the allowed timeframe, possibly due to network issues or server load. + """ class DoesNotExistError(ApiError): - pass + """ + Error raised when a requested resource does not exist. + + This exception is triggered when a specific resource is requested + but cannot be found in the API, indicating a potential data mismatch + or invalid identifier. + """ class ValidationError(ApiError): - pass + """ + Error raised for invalid input data. + + This exception is raised when the input data for an API request + does not meet validation requirements, such as incorrect data types + or missing fields. + """ diff --git a/tests/test_client.py b/tests/test_client.py index 2efd2df..1592501 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -3,6 +3,7 @@ import json import os import re +from typing import Any import pytest @@ -13,6 +14,17 @@ @pytest.fixture def simple_data() -> tuple[dict[str, list[dict[str, str]]], str]: + """ + This function provides a simple data structure and its encoding for testing purposes. + + Parameters: + None + + Returns: + tuple: A tuple containing two elements: + - A dictionary representing structured data with a list of dictionaries. + - A string representing the encoding of the data. + """ data: dict[str, list[dict[str, str]]] = { "Data": [{"Name": "first_name", "Value": "John"}] } @@ -22,6 +34,15 @@ def simple_data() -> tuple[dict[str, list[dict[str, str]]], str]: @pytest.fixture def client_mj30() -> Client: + """ + This function creates and returns a Mailjet API client instance for version 3.0. + + Parameters: + None + + Returns: + Client: An instance of the Mailjet API client configured for version 3.0. The client is authenticated using the public and private API keys provided as environment variables. + """ auth: tuple[str, str] = ( os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"], @@ -29,8 +50,43 @@ def client_mj30() -> Client: return Client(auth=auth) +@pytest.fixture +def client_mj30_invalid_auth() -> Client: + """ + This function creates and returns a Mailjet API client instance for version 3.0, + but with invalid authentication credentials. + + Parameters: + None + + Returns: + Client: An instance of the Mailjet API client configured for version 3.0. + The client is authenticated using invalid public and private API keys. + If the client is used to make requests, it will raise a ValueError. + """ + auth: tuple[str, str] = ( + "invalid_public_key", + "invalid_private_key", + ) + return Client(auth=auth) + + @pytest.fixture def client_mj31() -> Client: + """ + This function creates and returns a Mailjet API client instance for version 3.1. + + Parameters: + None + + Returns: + Client: An instance of the Mailjet API client configured for version 3.1. + The client is authenticated using the public and private API keys provided as environment variables. + + Note: + - The function retrieves the public and private API keys from the environment variables 'MJ_APIKEY_PUBLIC' and 'MJ_APIKEY_PRIVATE' respectively. + - The client is initialized with the provided authentication credentials and the version set to 'v3.1'. + """ auth: tuple[str, str] = ( os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"], @@ -44,6 +100,17 @@ def client_mj31() -> Client: def test_json_data_str_or_bytes_with_ensure_ascii( simple_data: tuple[dict[str, list[dict[str, str]]], str] ) -> None: + """ + This function tests the conversion of structured data into JSON format with the specified encoding settings. + + Parameters: + simple_data (tuple[dict[str, list[dict[str, str]]], str]): A tuple containing two elements: + - A dictionary representing structured data with a list of dictionaries. + - A string representing the encoding of the data. + + Returns: + None: The function does not return any value. It performs assertions to validate the JSON conversion. + """ data, data_encoding = simple_data ensure_ascii: bool = True @@ -60,6 +127,18 @@ def test_json_data_str_or_bytes_with_ensure_ascii( def test_json_data_str_or_bytes_with_ensure_ascii_false( simple_data: tuple[dict[str, list[dict[str, str]]], str] ) -> None: + """ + This function tests the conversion of structured data into JSON format with the specified encoding settings. + It specifically tests the case where the 'ensure_ascii' parameter is set to False. + + Parameters: + simple_data (tuple[dict[str, list[dict[str, str]]], str]): A tuple containing two elements: + - A dictionary representing structured data with a list of dictionaries. + - A string representing the encoding of the data. + + Returns: + None: The function does not return any value. It performs assertions to validate the JSON conversion. + """ data, data_encoding = simple_data ensure_ascii: bool = False @@ -76,6 +155,17 @@ def test_json_data_str_or_bytes_with_ensure_ascii_false( def test_json_data_is_none( simple_data: tuple[dict[str, list[dict[str, str]]], str] ) -> None: + """ + This function tests the conversion of structured data into JSON format when the data is None. + + Parameters: + simple_data (tuple[dict[str, list[dict[str, str]]], str]): A tuple containing two elements: + - A dictionary representing structured data with a list of dictionaries. + - A string representing the encoding of the data. + + Returns: + None: The function does not return any value. It performs assertions to validate the JSON conversion. + """ data, data_encoding = simple_data ensure_ascii: bool = True data: dict[str, list[dict[str, str]]] | None = None # type: ignore @@ -122,3 +212,75 @@ def test_prepare_url_headers_and_url() -> None: def test_post_with_no_param(client_mj30: Client) -> None: result = client_mj30.sender.create(data={}).json() assert "StatusCode" in result and result["StatusCode"] == 400 + + +def test_get_no_param(client_mj30: Client) -> None: + result: Any = client_mj30.contact.get().json() + assert "Data" in result and "Count" in result + + +def test_client_initialization_with_invalid_api_key( + client_mj30_invalid_auth: Client, +) -> None: + with pytest.raises(ValueError): + client_mj30_invalid_auth.contact.get().json() + + +def test_prepare_url_mixed_case_input() -> None: + """Test prepare_url with mixed case input""" + name: str = re.sub(r"[A-Z]", prepare_url, "contact") + config: Config = Config(version="v3", api_url="https://api.mailjet.com/") + url, headers = config[name] + assert url == "https://api.mailjet.com/v3/REST/contact" + assert headers == { + "Content-type": "application/json", + "User-agent": f"mailjet-apiv3-python/v{get_version()}", + } + + +def test_prepare_url_empty_input() -> None: + """Test prepare_url with empty input""" + name = re.sub(r"[A-Z]", prepare_url, "") + config = Config(version="v3", api_url="https://api.mailjet.com/") + url, headers = config[name] + assert url == "https://api.mailjet.com/v3/REST/" + assert headers == { + "Content-type": "application/json", + "User-agent": f"mailjet-apiv3-python/v{get_version()}", + } + + +def test_prepare_url_with_numbers_input_bad() -> None: + """Test prepare_url with input containing numbers""" + name = re.sub(r"[A-Z]", prepare_url, "contact1_managecontactslists1") + config = Config(version="v3", api_url="https://api.mailjet.com/") + url, headers = config[name] + assert url != "https://api.mailjet.com/v3/REST/contact" + assert headers == { + "Content-type": "application/json", + "User-agent": f"mailjet-apiv3-python/v{get_version()}", + } + + +def test_prepare_url_leading_trailing_underscores_input_bad() -> None: + """Test prepare_url with input containing leading and trailing underscores""" + name: str = re.sub(r"[A-Z]", prepare_url, "_contact_managecontactslists_") + config: Config = Config(version="v3", api_url="https://api.mailjet.com/") + url, headers = config[name] + assert url != "https://api.mailjet.com/v3/REST/contact" + assert headers == { + "Content-type": "application/json", + "User-agent": f"mailjet-apiv3-python/v{get_version()}", + } + + +def test_prepare_url_mixed_case_input_bad() -> None: + """Test prepare_url with mixed case input""" + name: str = re.sub(r"[A-Z]", prepare_url, "cOntact") + config: Config = Config(version="v3", api_url="https://api.mailjet.com/") + url, headers = config[name] + assert url != "https://api.mailjet.com/v3/REST/contact" + assert headers == { + "Content-type": "application/json", + "User-agent": f"mailjet-apiv3-python/v{get_version()}", + } From b2aae1a111905cc070b917849b2361f7199eb453 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:18:22 +0200 Subject: [PATCH 2/8] docs: Enable docstring lints, add package, module docstrings, add more docstrings to test.py --- mailjet_rest/__init__.py | 16 ++++ mailjet_rest/client.py | 109 +++++++++++++------------- mailjet_rest/utils/__init__.py | 7 ++ mailjet_rest/utils/version.py | 19 ++++- pyproject.toml | 3 - test.py | 137 +++++++++++++++++++++++++++++++++ 6 files changed, 231 insertions(+), 60 deletions(-) diff --git a/mailjet_rest/__init__.py b/mailjet_rest/__init__.py index cc550a8..df91474 100644 --- a/mailjet_rest/__init__.py +++ b/mailjet_rest/__init__.py @@ -1,3 +1,19 @@ +"""The `mailjet_rest` package provides a Python client for interacting with the Mailjet API. + +This package includes the main `Client` class for handling API requests, along with +utility functions for version management. The package exposes a consistent interface +for Mailjet API operations. + +Attributes: + __version__ (str): The current version of the `mailjet_rest` package. + __all__ (list): Specifies the public API of the package, including `Client` + for API interactions and `get_version` for retrieving version information. + +Modules: + - client: Defines the main API client. + - utils.version: Provides version management functionality. +""" + from mailjet_rest.client import Client from mailjet_rest.utils.version import get_version diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index e3e4a5a..e03ab4c 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -1,3 +1,31 @@ +"""This module provides the main client and helper classes for interacting with the Mailjet API. + +The `mailjet_rest.client` module includes the core `Client` class for managing +API requests, configuration, and error handling, as well as utility functions +and classes for building request headers, URLs, and parsing responses. + +Classes: + - Config: Manages configuration settings for the Mailjet API. + - Endpoint: Represents specific API endpoints and provides methods for + common HTTP operations like GET, POST, PUT, and DELETE. + - Client: The main API client for authenticating and making requests. + - ApiError: Base class for handling API-specific errors, with subclasses + for more specific error types (e.g., `AuthorizationError`, `TimeoutError`). + +Functions: + - prepare_url: Prepares URLs for API requests. + - api_call: A helper function that sends HTTP requests to the API and handles + responses. + - build_headers: Builds HTTP headers for the requests. + - build_url: Constructs the full API URL based on endpoint and parameters. + - parse_response: Parses API responses and handles error conditions. + +Exceptions: + - ApiError: Base exception for API errors, with subclasses to represent + specific error types, such as `AuthorizationError`, `TimeoutError`, + `ActionDeniedError`, and `ValidationError`. +""" + from __future__ import annotations import json @@ -23,8 +51,7 @@ def prepare_url(key: Match[str]) -> str: - """ - Replaces capital letters in the input string with a dash prefix and converts them to lowercase. + """Replaces capital letters in the input string with a dash prefix and converts them to lowercase. Parameters: key (Match[str]): A match object representing a substring from the input string. The substring should contain a single capital letter. @@ -39,8 +66,7 @@ def prepare_url(key: Match[str]) -> str: class Config: - """ - Configuration settings for interacting with the Mailjet API. + """Configuration settings for interacting with the Mailjet API. This class stores and manages API configuration details, including the API URL, version, and user agent string. It provides methods for initializing these settings @@ -59,8 +85,7 @@ class Config: user_agent: str = "mailjet-apiv3-python/v" + get_version() def __init__(self, version: str | None = None, api_url: str | None = None) -> None: - """ - Initialize a new Config instance with specified or default API settings. + """Initialize a new Config instance with specified or default API settings. This initializer sets the API version and base URL. If no version or URL is provided, it defaults to the predefined class values. @@ -77,8 +102,7 @@ def __init__(self, version: str | None = None, api_url: str | None = None) -> No self.api_url = api_url or self.DEFAULT_API_URL def __getitem__(self, key: str) -> tuple[str, dict[str, str]]: - """ - Retrieve the API endpoint URL and headers for a given key. + """Retrieve the API endpoint URL and headers for a given key. This method builds the URL and headers required for specific API interactions. The URL is adjusted based on the API version, and additional headers are @@ -117,8 +141,7 @@ def __getitem__(self, key: str) -> tuple[str, dict[str, str]]: class Endpoint: - """ - A class representing a specific Mailjet API endpoint. + """A class representing a specific Mailjet API endpoint. This class provides methods to perform HTTP requests to a given API endpoint, including GET, POST, PUT, and DELETE requests. It manages URL construction, @@ -146,8 +169,7 @@ def __init__( auth: tuple[str, str] | None, action: str | None = None, ) -> None: - """ - Initialize a new Endpoint instance. + """Initialize a new Endpoint instance. Args: url (str): The base URL for the endpoint. @@ -164,8 +186,7 @@ def _get( id: str | None = None, **kwargs: Any, ) -> Response: - """ - Perform an internal GET request to the endpoint. + """Perform an internal GET request to the endpoint. Constructs the URL with the provided filters and action_id to retrieve specific data from the API. @@ -197,8 +218,7 @@ def get_many( action_id: str | None = None, **kwargs: Any, ) -> Response: - """ - Performs a GET request to retrieve multiple resources. + """Performs a GET request to retrieve multiple resources. Parameters: - filters (Mapping[str, str | Any] | None): Filters to be applied in the request. @@ -217,8 +237,7 @@ def get( action_id: str | None = None, **kwargs: Any, ) -> Response: - """ - Performs a GET request to retrieve a specific resource. + """Performs a GET request to retrieve a specific resource. Parameters: - id (str | None): The ID of the specific resource to be retrieved. @@ -241,8 +260,7 @@ def create( data_encoding: str = "utf-8", **kwargs: Any, ) -> Response: - """ - Performs a POST request to create a new resource. + """Performs a POST request to create a new resource. Parameters: - data (dict | None): The data to include in the request body. @@ -284,8 +302,7 @@ def update( data_encoding: str = "utf-8", **kwargs: Any, ) -> Response: - """ - Performs a PUT request to update an existing resource. + """Performs a PUT request to update an existing resource. Parameters: - id (str | None): The ID of the specific resource to be updated. @@ -318,8 +335,7 @@ def update( ) def delete(self, id: str | None, **kwargs: Any) -> Response: - """ - Performs a DELETE request to delete a resource. + """Performs a DELETE request to delete a resource. Parameters: - id (str | None): The ID of the specific resource to be deleted. @@ -340,8 +356,7 @@ def delete(self, id: str | None, **kwargs: Any) -> Response: class Client: - """ - A client for interacting with the Mailjet API. + """A client for interacting with the Mailjet API. This class manages authentication, configuration, and API endpoint access. It initializes with API authentication details and uses dynamic attribute access @@ -357,8 +372,7 @@ class Client: """ def __init__(self, auth: tuple[str, str] | None = None, **kwargs: Any) -> None: - """ - Initialize a new Client instance for API interaction. + """Initialize a new Client instance for API interaction. This method sets up API authentication and configuration. The `auth` parameter provides a tuple with the API key and secret. Additional keyword arguments can @@ -381,8 +395,7 @@ def __init__(self, auth: tuple[str, str] | None = None, **kwargs: Any) -> None: self.config = Config(version=version, api_url=api_url) def __getattr__(self, name: str) -> Any: - """ - Dynamically access API endpoints as attributes. + """Dynamically access API endpoints as attributes. This method allows for flexible, attribute-style access to API endpoints. It constructs the appropriate endpoint URL and headers based on the attribute @@ -430,8 +443,7 @@ def api_call( action_id: str | None = None, **kwargs: Any, ) -> Response | Any: - """ - This function is responsible for making an API call to a specified URL using the provided method, headers, and other parameters. + """This function is responsible for making an API call to a specified URL using the provided method, headers, and other parameters. Parameters: - auth (tuple[str, str] | None): A tuple containing the API key and secret for authentication. @@ -489,8 +501,7 @@ def build_headers( action: str, extra_headers: dict[str, str] | None = None, ) -> dict[str, str]: - """ - Build headers based on resource and action. + """Build headers based on resource and action. Parameters: - resource (str): The name of the resource for which headers are being built. @@ -520,8 +531,7 @@ def build_url( resource_id: str | None = None, action_id: str | None = None, ) -> str: - """ - Construct a URL for making an API request. + """Construct a URL for making an API request. This function takes the base URL, method, action, resource ID, and action ID as parameters and constructs a URL by appending the resource ID, action, and action ID to the base URL. @@ -546,8 +556,7 @@ def build_url( def parse_response(response: Response, debug: bool = False) -> Any: - """ - Parse the response from an API request. + """Parse the response from an API request. This function extracts the JSON data from the response and logs debug information if the `debug` flag is set to True. @@ -573,8 +582,7 @@ def parse_response(response: Response, debug: bool = False) -> Any: class ApiError(Exception): - """ - Base class for all API-related errors. + """Base class for all API-related errors. This exception serves as the root for all custom API error types, allowing for more specific error handling based on the type of API @@ -583,8 +591,7 @@ class ApiError(Exception): class AuthorizationError(ApiError): - """ - Error raised for authorization failures. + """Error raised for authorization failures. This error is raised when the API request fails due to invalid or missing authentication credentials. @@ -592,8 +599,7 @@ class AuthorizationError(ApiError): class ActionDeniedError(ApiError): - """ - Error raised when an action is denied by the API. + """Error raised when an action is denied by the API. This exception is triggered when an action is requested but is not permitted, likely due to insufficient permissions. @@ -601,8 +607,7 @@ class ActionDeniedError(ApiError): class CriticalApiError(ApiError): - """ - Error raised for critical API failures. + """Error raised for critical API failures. This error represents severe issues with the API or infrastructure that prevent requests from completing. @@ -610,8 +615,7 @@ class CriticalApiError(ApiError): class ApiRateLimitError(ApiError): - """ - Error raised when the API rate limit is exceeded. + """Error raised when the API rate limit is exceeded. This exception is raised when the user has made too many requests within a given time frame, as enforced by the API's rate limit policy. @@ -619,8 +623,7 @@ class ApiRateLimitError(ApiError): class TimeoutError(ApiError): - """ - Error raised when an API request times out. + """Error raised when an API request times out. This error is raised if an API request does not complete within the allowed timeframe, possibly due to network issues or server load. @@ -628,8 +631,7 @@ class TimeoutError(ApiError): class DoesNotExistError(ApiError): - """ - Error raised when a requested resource does not exist. + """Error raised when a requested resource does not exist. This exception is triggered when a specific resource is requested but cannot be found in the API, indicating a potential data mismatch @@ -638,8 +640,7 @@ class DoesNotExistError(ApiError): class ValidationError(ApiError): - """ - Error raised for invalid input data. + """Error raised for invalid input data. This exception is raised when the input data for an API request does not meet validation requirements, such as incorrect data types diff --git a/mailjet_rest/utils/__init__.py b/mailjet_rest/utils/__init__.py index e69de29..21b7c8b 100644 --- a/mailjet_rest/utils/__init__.py +++ b/mailjet_rest/utils/__init__.py @@ -0,0 +1,7 @@ +"""The `mailjet_rest.utils` package provides utility functions for interacting with the package versionI. + +This package includes a module for managing the package version. + +Modules: + - version: Manages the package versioning. +""" diff --git a/mailjet_rest/utils/version.py b/mailjet_rest/utils/version.py index 2955c71..e5ad95d 100644 --- a/mailjet_rest/utils/version.py +++ b/mailjet_rest/utils/version.py @@ -1,3 +1,16 @@ +"""Versioning utilities for the Mailjet REST API client. + +This module defines the current version of the Mailjet client and provides +a helper function, `get_version`, to retrieve the version as a formatted string. + +Attributes: + VERSION (tuple[int, int, int]): A tuple representing the major, minor, and patch + version of the package. + +Functions: + get_version: Returns the version as a string in the format "major.minor.patch". +""" + from __future__ import annotations @@ -5,9 +18,9 @@ def get_version(version: tuple[int, ...] | None = None) -> str: - """ - Calculate package version based on a 3 item tuple. - In addition verify that the tuple contains 3 items + """Calculate package version based on a 3 item tuple. + + In addition verify that the tuple contains 3 items. """ if version is None: version = VERSION diff --git a/pyproject.toml b/pyproject.toml index 83e5a6b..9dea2cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -197,9 +197,6 @@ ignore = [ "B904", # Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` # pycodestyle (E, W) "CPY001", # Missing copyright notice at top of file - # TODO: Enable docstring lints when they will be available. - "D", - "DOC", "E501", "FBT001", # Boolean-typed positional argument in function definition "FBT002", # Boolean default positional argument in function definition diff --git a/test.py b/test.py index b327115..c2d5f62 100644 --- a/test.py +++ b/test.py @@ -1,3 +1,5 @@ +"""A suite of tests for Mailjet API client functionality.""" + import os import random import string @@ -8,7 +10,29 @@ class TestSuite(unittest.TestCase): + """A suite of tests for Mailjet API client functionality. + + This class provides setup and teardown functionality for tests involving the + Mailjet API client, with authentication and client initialization handled + in `setUp`. Each test in this suite operates with the configured Mailjet client + instance to simulate API interactions. + """ + def setUp(self) -> None: + """Set up the test environment by initializing authentication credentials and the Mailjet client. + + This method is called before each test to ensure a consistent testing + environment. It retrieves the API keys from environment variables and + uses them to create an instance of the Mailjet `Client` for authenticated + API interactions. + + Attributes: + - self.auth (tuple[str, str]): A tuple containing the public and private API keys obtained from the environment variables 'MJ_APIKEY_PUBLIC' and 'MJ_APIKEY_PRIVATE' respectively. + - self.client (Client): An instance of the Mailjet Client class, initialized with the provided authentication credentials. + + Returns: + None + """ self.auth: tuple[str, str] = ( os.environ["MJ_APIKEY_PUBLIC"], os.environ["MJ_APIKEY_PRIVATE"], @@ -16,24 +40,86 @@ def setUp(self) -> None: self.client: Client = Client(auth=self.auth) def test_get_no_param(self) -> None: + """This test function sends a GET request to the Mailjet API endpoint for contacts without any parameters. + + It verifies that the response contains 'Data' and 'Count' fields. + + Parameters: + None + + Returns: + None + """ result: Any = self.client.contact.get().json() self.assertTrue("Data" in result and "Count" in result) def test_get_valid_params(self) -> None: + """This test function sends a GET request to the Mailjet API endpoint for contacts with a valid parameter 'limit'. + + It verifies that the response contains a count of contacts that is within the range of 0 to 2. + + Parameters: + None + + Returns: + None + """ result: Any = self.client.contact.get(filters={"limit": 2}).json() self.assertTrue(result["Count"] >= 0 or result["Count"] <= 2) def test_get_invalid_parameters(self) -> None: + """This test function sends a GET request to the Mailjet API endpoint for contacts with an invalid parameter. + + It verifies that the response contains 'Count' field, demonstrating that invalid parameters are ignored. + + Parameters: + None + + Returns: + None + """ # invalid parameters are ignored result: Any = self.client.contact.get(filters={"invalid": "false"}).json() self.assertTrue("Count" in result) def test_get_with_data(self) -> None: + """This test function sends a GET request to the Mailjet API endpoint for contacts with 'data' parameter. + + It verifies that the request is successful (status code 200) and does not use the 'data' parameter. + + Parameters: + None + + Returns: + None + """ # it shouldn't use data result = self.client.contact.get(data={"Email": "api@mailjet.com"}) self.assertTrue(result.status_code == 200) def test_get_with_action(self) -> None: + """This function tests the functionality of adding a contact to a contact list using the Mailjet API client. + + It first retrieves a contact and a contact list from the API, then adds the contact to the list. + Finally, it verifies that the contact has been successfully added to the list. + + Parameters: + None + + Returns: + None + + Attributes: + - get_contact (Any): The result of the initial contact retrieval, containing a single contact. + - contact_id (str): The ID of the retrieved contact. + - post_contact (Response): The response from creating a new contact if no contact was found. + - get_contact_list (Any): The result of the contact list retrieval, containing a single contact list. + - list_id (str): The ID of the retrieved contact list. + - post_contact_list (Response): The response from creating a new contact list if no contact list was found. + - data (dict[str, list[dict[str, str]]]): The data for managing contact lists, containing the list ID and action to add the contact. + - result_add_list (Response): The response from adding the contact to the contact list. + - result (Any): The result of retrieving the contact's contact lists, containing the count of contact lists. + """ get_contact: Any = self.client.contact.get(filters={"limit": 1}).json() if get_contact["Count"] != 0: contact_id: str = get_contact["Data"][0]["ID"] @@ -83,6 +169,20 @@ def test_get_with_action(self) -> None: self.assertTrue("Count" in result) def test_get_with_id_filter(self) -> None: + """This test function sends a GET request to the Mailjet API endpoint for contacts with a specific email address obtained from a previous contact retrieval. + + It verifies that the response contains a contact with the same email address as the one used in the filter. + + Parameters: + None + + Returns: + None + + Attributes: + - result_contact (Any): The result of the initial contact retrieval, containing a single contact. + - result_contact_with_id (Any): The result of the contact retrieval using the email address from the initial contact as a filter. + """ result_contact: Any = self.client.contact.get(filters={"limit": 1}).json() result_contact_with_id: Any = self.client.contact.get( filter={"Email": result_contact["Data"][0]["Email"]}, @@ -93,10 +193,35 @@ def test_get_with_id_filter(self) -> None: ) def test_post_with_no_param(self) -> None: + """This function tests the behavior of the Mailjet API client when attempting to create a sender with no parameters. + + The function sends a POST request to the Mailjet API endpoint for creating a sender with an empty + data dictionary. It then verifies that the response contains a 'StatusCode' field with a value of 400, + indicating a bad request. This test ensures that the client handles missing required parameters + appropriately. + + Parameters: + None + + Returns: + None + """ result: Any = self.client.sender.create(data={}).json() self.assertTrue("StatusCode" in result and result["StatusCode"] == 400) def test_client_custom_version(self) -> None: + """This test function verifies the functionality of setting a custom version for the Mailjet API client. + + The function initializes a new instance of the Mailjet Client with custom version "v3.1". + It then asserts that the client's configuration version is correctly set to "v3.1". + Additionally, it verifies that the send endpoint URL in the client's configuration is updated to the correct version. + + Parameters: + None + + Returns: + None + """ self.client = Client(auth=self.auth, version="v3.1") self.assertEqual(self.client.config.version, "v3.1") self.assertEqual( @@ -105,6 +230,18 @@ def test_client_custom_version(self) -> None: ) def test_user_agent(self) -> None: + """This function tests the user agent configuration of the Mailjet API client. + + The function initializes a new instance of the Mailjet Client with a custom version "v3.1". + It then asserts that the client's user agent is correctly set to "mailjet-apiv3-python/v1.3.5". + This test ensures that the client's user agent is properly configured and includes the correct version information. + + Parameters: + None + + Returns: + None + """ self.client = Client(auth=self.auth, version="v3.1") self.assertEqual(self.client.config.user_agent, "mailjet-apiv3-python/v1.3.5") From dbbcac0d9e89beaf712795a29b7c817c01383c24 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:31:59 +0200 Subject: [PATCH 3/8] docs: Remove Return: None --- mailjet_rest/client.py | 7 ------- mailjet_rest/utils/version.py | 13 ++++++++++++- pyproject.toml | 3 ++- test.py | 30 ------------------------------ 4 files changed, 14 insertions(+), 39 deletions(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index e03ab4c..fe3f10c 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -93,9 +93,6 @@ def __init__(self, version: str | None = None, api_url: str | None = None) -> No Parameters: - version (str | None): The API version to use. If None, the default version ('v3') is used. - api_url (str | None): The base URL for API requests. If None, the default URL (DEFAULT_API_URL) is used. - - Returns: - - None """ if version is not None: self.version = version @@ -382,10 +379,6 @@ def __init__(self, auth: tuple[str, str] | None = None, **kwargs: Any) -> None: - auth (tuple[str, str] | None): A tuple containing the API key and secret for authentication. If None, authentication is not required. - **kwargs (Any): Additional keyword arguments, such as `version` and `api_url`, for configuring the client. - Returns: - - None - - Example: client = Client(auth=("api_key", "api_secret"), version="v3") """ diff --git a/mailjet_rest/utils/version.py b/mailjet_rest/utils/version.py index e5ad95d..48abae5 100644 --- a/mailjet_rest/utils/version.py +++ b/mailjet_rest/utils/version.py @@ -20,7 +20,18 @@ def get_version(version: tuple[int, ...] | None = None) -> str: """Calculate package version based on a 3 item tuple. - In addition verify that the tuple contains 3 items. + In addition, verify that the tuple contains 3 items. + + Parameters: + version (tuple[int, ...], optional): A tuple representing the version of the package. + If not provided, the function will use the VERSION constant. + Default is None. + + Returns: + str: The version as a string in the format "major.minor.patch". + + Raises: + ValueError: If the provided tuple does not contain exactly 3 items. """ if version is None: version = VERSION diff --git a/pyproject.toml b/pyproject.toml index 9dea2cb..e061629 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -197,6 +197,7 @@ ignore = [ "B904", # Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` # pycodestyle (E, W) "CPY001", # Missing copyright notice at top of file + "DOC501", # DOC501 Raised exception `TimeoutError` and `ApiError` missing from docstring "E501", "FBT001", # Boolean-typed positional argument in function definition "FBT002", # Boolean default positional argument in function definition @@ -210,7 +211,7 @@ ignore = [ "PLE0604", "PLR2004", # PLR2004 Magic value used in comparison, consider replacing `XXX` with a constant variable "PLR0913", # PLR0913 Too many arguments in function definition (6 > 5) - #"PLR0917", # PLR0917 Too many positional arguments + "PLR0917", # PLR0917 Too many positional arguments # TODO: "Remove Q000 it before the next release "Q000", "Q003", # Checks for avoidable escaped quotes ("\"" -> '"') diff --git a/test.py b/test.py index c2d5f62..7b03ebc 100644 --- a/test.py +++ b/test.py @@ -29,9 +29,6 @@ def setUp(self) -> None: Attributes: - self.auth (tuple[str, str]): A tuple containing the public and private API keys obtained from the environment variables 'MJ_APIKEY_PUBLIC' and 'MJ_APIKEY_PRIVATE' respectively. - self.client (Client): An instance of the Mailjet Client class, initialized with the provided authentication credentials. - - Returns: - None """ self.auth: tuple[str, str] = ( os.environ["MJ_APIKEY_PUBLIC"], @@ -46,9 +43,6 @@ def test_get_no_param(self) -> None: Parameters: None - - Returns: - None """ result: Any = self.client.contact.get().json() self.assertTrue("Data" in result and "Count" in result) @@ -60,9 +54,6 @@ def test_get_valid_params(self) -> None: Parameters: None - - Returns: - None """ result: Any = self.client.contact.get(filters={"limit": 2}).json() self.assertTrue(result["Count"] >= 0 or result["Count"] <= 2) @@ -74,9 +65,6 @@ def test_get_invalid_parameters(self) -> None: Parameters: None - - Returns: - None """ # invalid parameters are ignored result: Any = self.client.contact.get(filters={"invalid": "false"}).json() @@ -89,9 +77,6 @@ def test_get_with_data(self) -> None: Parameters: None - - Returns: - None """ # it shouldn't use data result = self.client.contact.get(data={"Email": "api@mailjet.com"}) @@ -106,9 +91,6 @@ def test_get_with_action(self) -> None: Parameters: None - Returns: - None - Attributes: - get_contact (Any): The result of the initial contact retrieval, containing a single contact. - contact_id (str): The ID of the retrieved contact. @@ -176,9 +158,6 @@ def test_get_with_id_filter(self) -> None: Parameters: None - Returns: - None - Attributes: - result_contact (Any): The result of the initial contact retrieval, containing a single contact. - result_contact_with_id (Any): The result of the contact retrieval using the email address from the initial contact as a filter. @@ -202,9 +181,6 @@ def test_post_with_no_param(self) -> None: Parameters: None - - Returns: - None """ result: Any = self.client.sender.create(data={}).json() self.assertTrue("StatusCode" in result and result["StatusCode"] == 400) @@ -218,9 +194,6 @@ def test_client_custom_version(self) -> None: Parameters: None - - Returns: - None """ self.client = Client(auth=self.auth, version="v3.1") self.assertEqual(self.client.config.version, "v3.1") @@ -238,9 +211,6 @@ def test_user_agent(self) -> None: Parameters: None - - Returns: - None """ self.client = Client(auth=self.auth, version="v3.1") self.assertEqual(self.client.config.user_agent, "mailjet-apiv3-python/v1.3.5") From f2fcbce0260d07776bde2a5f821250ba53ccb473 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:41:20 +0200 Subject: [PATCH 4/8] docs: Remove unused or fixed ruff lint rules --- pyproject.toml | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e061629..2590934 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -201,27 +201,16 @@ ignore = [ "E501", "FBT001", # Boolean-typed positional argument in function definition "FBT002", # Boolean default positional argument in function definition - "INP001", # INP001 File `samples/campaign_sample.py` is part of an implicit namespace package. Add an `__init__.py`. - "PD901", - "PD015", - # pep8-naming (N) - "N802", - "N806", - # TODO: PLE0604 Invalid object in `__all__`, must contain only strings - "PLE0604", + # TODO: Replace with http.HTTPStatus, see https://docs.python.org/3/library/http.html#http-status-codes "PLR2004", # PLR2004 Magic value used in comparison, consider replacing `XXX` with a constant variable "PLR0913", # PLR0913 Too many arguments in function definition (6 > 5) "PLR0917", # PLR0917 Too many positional arguments - # TODO: "Remove Q000 it before the next release - "Q000", "Q003", # Checks for avoidable escaped quotes ("\"" -> '"') - "RET504", # RET504 Unnecessary assignment to `response` before `return` statement - "RUF012", # TODO:" PT009 Use a regular `assert` instead of unittest-style `assertTrue` "PT009", "S311", # S311 Standard pseudo-random generators are not suitable for cryptographic purposes + # TODO: T201 Replace `print` with logging functions "T201", # T201 `print` found - "UP031", # pyupgrade (UP): Skip for logging: UP031 Use format specifiers instead of percent format ] From c3437f9a11d7fe4afe6913e9e74252a849644833 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:49:25 +0200 Subject: [PATCH 5/8] ci: update pre-commit hooks --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 86a1da6..9391846 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -117,7 +117,7 @@ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: v0.7.1 + rev: v0.7.2 hooks: # Run the linter. - id: ruff @@ -141,7 +141,7 @@ repos: exclude: ^samples/ - repo: https://github.com/RobertCraigie/pyright-python - rev: v1.1.387 + rev: v1.1.388 hooks: - id: pyright @@ -155,7 +155,7 @@ repos: additional_dependencies: [".[toml]"] - repo: https://github.com/crate-ci/typos - rev: v1.26.8 + rev: v1.27.0 hooks: - id: typos From 25efdfe5d002817e50e62ee4579967a7484b4af7 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Thu, 7 Nov 2024 19:18:57 +0200 Subject: [PATCH 6/8] Add more docstring to tests --- tests/test_client.py | 125 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 117 insertions(+), 8 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 1592501..9dee0e3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -181,21 +181,53 @@ def test_json_data_is_none( def test_prepare_url_list_splitting() -> None: - """Test prepare_url: list splitting""" + """This function tests the prepare_url function by splitting a string containing underscores and converting the first letter of each word to uppercase. + + The function then compares the resulting list with an expected list. + + Parameters: + None + + Note: + - The function uses the re.sub method to replace uppercase letters with the prepare_url function. + - It splits the resulting string into a list using the underscore as the delimiter. + - It asserts that the resulting list is equal to the expected list ["contact", "managecontactslists"]. + """ name: str = re.sub(r"[A-Z]", prepare_url, "contact_managecontactslists") split: list[str] = name.split("_") # noqa: FURB184 assert split == ["contact", "managecontactslists"] def test_prepare_url_first_list_element() -> None: - """Test prepare_url: list splitting, the first element, url, and headers""" + """This function tests the prepare_url function by splitting a string containing underscores, and retrieving the first element of the resulting list. + + Parameters: + None + + Note: + - The function uses the re.sub method to replace uppercase letters with the prepare_url function. + - It splits the resulting string into a list using the underscore as the delimiter. + - It asserts that the first element of the split list is equal to "contact". + """ name: str = re.sub(r"[A-Z]", prepare_url, "contact_managecontactslists") fname: str = name.split("_")[0] assert fname == "contact" def test_prepare_url_headers_and_url() -> None: - """Test prepare_url: list splitting, the first element, url, and headers""" + """Test the prepare_url function by splitting a string containing underscores, and retrieving the first element of the resulting list. + + Additionally, this test verifies the URL and headers generated by the prepare_url function. + + Parameters: + None + + Note: + - The function uses the re.sub method to replace uppercase letters with the prepare_url function. + - It creates a Config object with the specified version and API URL. + - It retrieves the URL and headers from the Config object using the modified string as the key. + - It asserts that the URL is equal to "https://api.mailjet.com/v3/REST/contact" and that the headers match the expected headers. + """ name: str = re.sub(r"[A-Z]", prepare_url, "contact_managecontactslists") config: Config = Config(version="v3", api_url="https://api.mailjet.com/") url, headers = config[name] @@ -222,12 +254,37 @@ def test_get_no_param(client_mj30: Client) -> None: def test_client_initialization_with_invalid_api_key( client_mj30_invalid_auth: Client, ) -> None: + """This function tests the initialization of a Mailjet API client with invalid authentication credentials. + + Parameters: + client_mj30_invalid_auth (Client): An instance of the Mailjet API client configured for version 3.0. + The client is authenticated using invalid public and private API keys. + + Returns: + None: The function does not return any value. It is expected to raise a ValueError when the client is used to make requests. + + Note: + - The function uses the pytest.raises context manager to assert that a ValueError is raised when the client's contact.get() method is called. + """ with pytest.raises(ValueError): client_mj30_invalid_auth.contact.get().json() def test_prepare_url_mixed_case_input() -> None: - """Test prepare_url with mixed case input""" + """Test prepare_url function with mixed case input. + + This function tests the prepare_url function by providing a string with mixed case characters. + It then compares the resulting URL with the expected URL. + + Parameters: + None + + Note: + - The function uses the re.sub method to replace uppercase letters with the prepare_url function. + - It creates a Config object with the specified version and API URL. + - It retrieves the URL and headers from the Config object using the modified string as the key. + - It asserts that the URL is equal to the expected URL and that the headers match the expected headers. + """ name: str = re.sub(r"[A-Z]", prepare_url, "contact") config: Config = Config(version="v3", api_url="https://api.mailjet.com/") url, headers = config[name] @@ -239,7 +296,20 @@ def test_prepare_url_mixed_case_input() -> None: def test_prepare_url_empty_input() -> None: - """Test prepare_url with empty input""" + """Test prepare_url function with empty input. + + This function tests the prepare_url function by providing an empty string as input. + It then compares the resulting URL with the expected URL. + + Parameters: + None + + Note: + - The function uses the re.sub method to replace uppercase letters with the prepare_url function. + - It creates a Config object with the specified version and API URL. + - It retrieves the URL and headers from the Config object using the modified string as the key. + - It asserts that the URL is equal to the expected URL and that the headers match the expected headers. + """ name = re.sub(r"[A-Z]", prepare_url, "") config = Config(version="v3", api_url="https://api.mailjet.com/") url, headers = config[name] @@ -251,7 +321,20 @@ def test_prepare_url_empty_input() -> None: def test_prepare_url_with_numbers_input_bad() -> None: - """Test prepare_url with input containing numbers""" + """Test the prepare_url function with input containing numbers. + + This function tests the prepare_url function by providing a string with numbers. + It then compares the resulting URL with the expected URL. + + Parameters: + None + + Note: + - The function uses the re.sub method to replace uppercase letters with the prepare_url function. + - It creates a Config object with the specified version and API URL. + - It retrieves the URL and headers from the Config object using the modified string as the key. + - It asserts that the URL is not equal to the expected URL and that the headers match the expected headers. + """ name = re.sub(r"[A-Z]", prepare_url, "contact1_managecontactslists1") config = Config(version="v3", api_url="https://api.mailjet.com/") url, headers = config[name] @@ -263,7 +346,20 @@ def test_prepare_url_with_numbers_input_bad() -> None: def test_prepare_url_leading_trailing_underscores_input_bad() -> None: - """Test prepare_url with input containing leading and trailing underscores""" + """Test prepare_url function with input containing leading and trailing underscores. + + This function tests the prepare_url function by providing a string with leading and trailing underscores. + It then compares the resulting URL with the expected URL. + + Parameters: + None + + Note: + - The function uses the re.sub method to replace uppercase letters with the prepare_url function. + - It creates a Config object with the specified version and API URL. + - It retrieves the URL and headers from the Config object using the modified string as the key. + - It asserts that the URL is not equal to the expected URL and that the headers match the expected headers. + """ name: str = re.sub(r"[A-Z]", prepare_url, "_contact_managecontactslists_") config: Config = Config(version="v3", api_url="https://api.mailjet.com/") url, headers = config[name] @@ -275,7 +371,20 @@ def test_prepare_url_leading_trailing_underscores_input_bad() -> None: def test_prepare_url_mixed_case_input_bad() -> None: - """Test prepare_url with mixed case input""" + """Test prepare_url function with mixed case input. + + This function tests the prepare_url function by providing a string with mixed case characters. + It then compares the resulting URL with the expected URL. + + Parameters: + None + + Note: + - The function uses the re.sub method to replace uppercase letters with the prepare_url function. + - It creates a Config object with the specified version and API URL. + - It retrieves the URL and headers from the Config object using the modified string as the key. + - It asserts that the URL is not equal to the expected URL and that the headers match the expected headers. + """ name: str = re.sub(r"[A-Z]", prepare_url, "cOntact") config: Config = Config(version="v3", api_url="https://api.mailjet.com/") url, headers = config[name] From fe6e3bb3481b80cf7964f1d75ca3cfb428e4dd13 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Thu, 7 Nov 2024 19:30:38 +0200 Subject: [PATCH 7/8] Add more docstring to tests --- tests/test_client.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 9dee0e3..d0598af 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -242,11 +242,36 @@ def test_prepare_url_headers_and_url() -> None: def test_post_with_no_param(client_mj30: Client) -> None: + """Tests a POST request with an empty data payload. + + This test sends a POST request to the 'create' endpoint using an empty dictionary + as the data payload. It checks that the API responds with a 400 status code, + indicating a bad request due to missing required parameters. + + Parameters: + client_mj30 (Client): An instance of the Mailjet API client. + + Raises: + AssertionError: If "StatusCode" is not in the result or if its value + is not 400. + """ result = client_mj30.sender.create(data={}).json() assert "StatusCode" in result and result["StatusCode"] == 400 def test_get_no_param(client_mj30: Client) -> None: + """Tests a GET request to retrieve contact data without any parameters. + + This test sends a GET request to the 'contact' endpoint without filters or + additional parameters. It verifies that the response includes both "Data" + and "Count" fields, confirming the endpoint returns a valid structure. + + Parameters: + client_mj30 (Client): An instance of the Mailjet API client. + + Raises: + AssertionError: If "Data" or "Count" are not present in the response. + """ result: Any = client_mj30.contact.get().json() assert "Data" in result and "Count" in result From 98ecf81e27a1ea12a7513486ed7e6394312384ec Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Sun, 10 Nov 2024 13:00:46 +0200 Subject: [PATCH 8/8] docs: pydocstyle linting --- mailjet_rest/client.py | 14 +++++++------- pyproject.toml | 1 - tests/__init__.py | 6 ++++++ tests/test_client.py | 17 ++++++----------- tests/test_version.py | 1 + 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index fe3f10c..d369350 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -51,7 +51,7 @@ def prepare_url(key: Match[str]) -> str: - """Replaces capital letters in the input string with a dash prefix and converts them to lowercase. + """Replace capital letters in the input string with a dash prefix and converts them to lowercase. Parameters: key (Match[str]): A match object representing a substring from the input string. The substring should contain a single capital letter. @@ -215,7 +215,7 @@ def get_many( action_id: str | None = None, **kwargs: Any, ) -> Response: - """Performs a GET request to retrieve multiple resources. + """Perform a GET request to retrieve multiple resources. Parameters: - filters (Mapping[str, str | Any] | None): Filters to be applied in the request. @@ -234,7 +234,7 @@ def get( action_id: str | None = None, **kwargs: Any, ) -> Response: - """Performs a GET request to retrieve a specific resource. + """Perform a GET request to retrieve a specific resource. Parameters: - id (str | None): The ID of the specific resource to be retrieved. @@ -257,7 +257,7 @@ def create( data_encoding: str = "utf-8", **kwargs: Any, ) -> Response: - """Performs a POST request to create a new resource. + """Perform a POST request to create a new resource. Parameters: - data (dict | None): The data to include in the request body. @@ -299,7 +299,7 @@ def update( data_encoding: str = "utf-8", **kwargs: Any, ) -> Response: - """Performs a PUT request to update an existing resource. + """Perform a PUT request to update an existing resource. Parameters: - id (str | None): The ID of the specific resource to be updated. @@ -332,7 +332,7 @@ def update( ) def delete(self, id: str | None, **kwargs: Any) -> Response: - """Performs a DELETE request to delete a resource. + """Perform a DELETE request to delete a resource. Parameters: - id (str | None): The ID of the specific resource to be deleted. @@ -436,7 +436,7 @@ def api_call( action_id: str | None = None, **kwargs: Any, ) -> Response | Any: - """This function is responsible for making an API call to a specified URL using the provided method, headers, and other parameters. + """Make an API call to a specified URL using the provided method, headers, and other parameters. Parameters: - auth (tuple[str, str] | None): A tuple containing the API key and secret for authentication. diff --git a/pyproject.toml b/pyproject.toml index 2590934..1c0593e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -276,7 +276,6 @@ ignore-overlong-task-comments = true [tool.ruff.lint.pydocstyle] convention = "google" - [tool.mypy] strict = true # Adapted from this StackOverflow post: diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..3e9246c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,6 @@ +"""The `tests` package contains unit and integration tests for the Mailjet REST API client. + +This package ensures that all core functionalities of the Mailjet client, including +authentication, API requests, error handling, and response parsing, work as expected. +Each module within this package tests a specific aspect or component of the client. +""" diff --git a/tests/test_client.py b/tests/test_client.py index d0598af..35a7943 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -14,8 +14,7 @@ @pytest.fixture def simple_data() -> tuple[dict[str, list[dict[str, str]]], str]: - """ - This function provides a simple data structure and its encoding for testing purposes. + """This function provides a simple data structure and its encoding for testing purposes. Parameters: None @@ -34,8 +33,7 @@ def simple_data() -> tuple[dict[str, list[dict[str, str]]], str]: @pytest.fixture def client_mj30() -> Client: - """ - This function creates and returns a Mailjet API client instance for version 3.0. + """This function creates and returns a Mailjet API client instance for version 3.0. Parameters: None @@ -52,9 +50,7 @@ def client_mj30() -> Client: @pytest.fixture def client_mj30_invalid_auth() -> Client: - """ - This function creates and returns a Mailjet API client instance for version 3.0, - but with invalid authentication credentials. + """This function creates and returns a Mailjet API client instance for version 3.0, but with invalid authentication credentials. Parameters: None @@ -73,8 +69,7 @@ def client_mj30_invalid_auth() -> Client: @pytest.fixture def client_mj31() -> Client: - """ - This function creates and returns a Mailjet API client instance for version 3.1. + """This function creates and returns a Mailjet API client instance for version 3.1. Parameters: None @@ -127,8 +122,8 @@ def test_json_data_str_or_bytes_with_ensure_ascii( def test_json_data_str_or_bytes_with_ensure_ascii_false( simple_data: tuple[dict[str, list[dict[str, str]]], str] ) -> None: - """ - This function tests the conversion of structured data into JSON format with the specified encoding settings. + """This function tests the conversion of structured data into JSON format with the specified encoding settings. + It specifically tests the case where the 'ensure_ascii' parameter is set to False. Parameters: diff --git a/tests/test_version.py b/tests/test_version.py index 7eb79dd..e74e9f0 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -23,6 +23,7 @@ def test_get_version_is_none() -> None: def test_get_version() -> None: """Test that package version is string. + Verify that if it's equal to tuple after splitting and mapped to tuple. """ result: str | tuple[int, ...]