From 23ce78f1888a0afa2f2ca5e0858a9e18d603d304 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Fri, 24 Apr 2026 17:50:56 +0300 Subject: [PATCH 1/8] build: Release candidate 1.6.0rc1 for testing a new refactored client --- mailjet_rest/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mailjet_rest/_version.py b/mailjet_rest/_version.py index 2d81c94..a91f75c 100644 --- a/mailjet_rest/_version.py +++ b/mailjet_rest/_version.py @@ -1 +1 @@ -__version__ = "1.5.1.post1.dev40" \ No newline at end of file +__version__ = "1.6.0rc1" \ No newline at end of file From 96e5f621f174faf6280d95766a32f2cfc445da0f Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Fri, 24 Apr 2026 18:00:39 +0300 Subject: [PATCH 2/8] docs: Update changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 163ffa3..f1659e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ We [keep a changelog.](http://keepachangelog.com/) ## [Unreleased] +## [1.6.0] - 2026-04-XX + ### Security - **CWE-22 (Prevented Path Traversal):** Prevented vulnerabilities by enforcing strict URL encoding (`urllib.parse.quote`) on all dynamically injected path parameters (`id` and `action_id`). @@ -68,6 +70,7 @@ We [keep a changelog.](http://keepachangelog.com/) ### Pull Requests Merged - [PR_125](https://github.com/mailjet/mailjet-apiv3-python/pull/125) - Refactor client. +- [PR_128](https://github.com/mailjet/mailjet-apiv3-python/pull/128) - Release 1.6.0. ## [1.5.1] - 2025-07-14 @@ -255,4 +258,5 @@ We [keep a changelog.](http://keepachangelog.com/) [1.4.0]: https://github.com/mailjet/mailjet-apiv3-python/releases/tag/v1.4.0 [1.5.0]: https://github.com/mailjet/mailjet-apiv3-python/releases/tag/v1.5.0 [1.5.1]: https://github.com/mailjet/mailjet-apiv3-python/releases/tag/v1.5.1 -[unreleased]: https://github.com/mailjet/mailjet-apiv3-python/releases/tag/v1.5.1...HEAD +[1.6.0]: https://github.com/mailjet/mailjet-apiv3-python/releases/tag/v1.6.0 +[unreleased]: https://github.com/mailjet/mailjet-apiv3-python/releases/tag/v1.6.0...HEAD From 4e1b590d170076bdd7cd73c2e68f8fe3e022612a Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Fri, 24 Apr 2026 18:27:26 +0300 Subject: [PATCH 3/8] docs: Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1659e6..fee388b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,7 +70,9 @@ We [keep a changelog.](http://keepachangelog.com/) ### Pull Requests Merged - [PR_125](https://github.com/mailjet/mailjet-apiv3-python/pull/125) - Refactor client. +- [PR_126](https://github.com/mailjet/mailjet-apiv3-python/pull/126) - build(deps): bump conda-incubator/setup-miniconda from 3.3.0 to 4.0.1 - [PR_128](https://github.com/mailjet/mailjet-apiv3-python/pull/128) - Release 1.6.0. +- [PR_129](https://github.com/mailjet/mailjet-apiv3-python/pull/129) - Use hyphen in the package name in readme. ## [1.5.1] - 2025-07-14 From 0bb01c225d18333953f1b3790f54bfd0e9082deb Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Sat, 25 Apr 2026 01:36:50 +0300 Subject: [PATCH 4/8] chore: Fix minor issues and typos --- README.md | 6 +++--- SECURITY.md | 2 +- mailjet_rest/client.py | 4 ++-- mailjet_rest/utils/version.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7633ee9..a580dad 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ with Client(auth=(api_key, api_secret), version="v3.1") as mailjet: (Note: > **Note** -> If you choose not to use the context manager, you should manually call mailjet.close() when your application shuts down). +> If you choose not to use the context manager, you should manually call mailjet.close() when your application shuts down. ### Advanced Configuration @@ -464,7 +464,7 @@ Some requests (for example [GET /contact](https://dev.mailjet.com/email/referenc `limit` `int` Limit the response to a select number of returned objects. Default value: `10`. Maximum value: `1000` `offset` `int` Retrieve a list of objects starting from a certain offset. Combine this query parameter with `limit` to retrieve a specific section of the list of objects. Default value: `0` `sort` `str` Sort the results by a property and select ascending (ASC) or descending (DESC) order. The default order is ascending. Keep in mind that this is not available for all properties. Default value: `ID asc` -Next example returns 40 contacts starting from 51th record sorted by `Email` field descendally: +Next example returns 40 contacts starting from 51st record sorted by `Email` field descendally: ```python filters = { @@ -645,7 +645,7 @@ Feel free to ask anything, and contribute: - Create a new branch. - Implement your feature or bug fix. - Add documentation to it. -- Commit, push, open a pull request and voila. +- Commit, push, open a pull request and voilĂ . If you have suggestions on how to improve the guides, please submit an issue in our [Official API Documentation repo](https://github.com/mailjet/api-documentation). diff --git a/SECURITY.md b/SECURITY.md index ff4cd01..35dc6ec 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -26,7 +26,7 @@ Please include the following details: If English is not your first language, please try to describe the problem and its impact to the best of your ability. For greater detail, -please use your native language and we will try our best to translate it +please use your native language, and we will try our best to translate it using online services. Please also include the code you used to find the problem and the diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index b99da24..5d2254d 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -141,7 +141,7 @@ def logging_handler(to_file: bool = False, **_kwargs: Any) -> logging.Logger: # Args: to_file (bool): Deprecated flag. Output is no longer written to files natively. - **kwargs (Any): Absorbs any other legacy keyword arguments. + **_kwargs (Any): Absorbs any other legacy keyword arguments. Returns: logging.Logger: A legacy logger instance to prevent AttributeError in old integrations. @@ -175,7 +175,7 @@ def parse_response( response (requests.Response): The HTTP response. log (Any, optional): Deprecated logging callable. debug (bool): Deprecated debug flag. - **kwargs (Any): Absorbs any other legacy keyword arguments. + **_kwargs (Any): Absorbs any other legacy keyword arguments. Returns: Any: The parsed JSON dictionary or raw text string. diff --git a/mailjet_rest/utils/version.py b/mailjet_rest/utils/version.py index b74fb9e..9c5a4fd 100644 --- a/mailjet_rest/utils/version.py +++ b/mailjet_rest/utils/version.py @@ -34,7 +34,7 @@ def clean_version(version_str: str) -> tuple[int, ...]: except (IndexError, ValueError): return 0, 0, 0 else: - return (major, minor, patch) + return major, minor, patch # VERSION is a tuple of integers (1, 3, 2). From c0500b63a025ee662ab1e9d0ed6d32ccbac4706e Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:15:01 +0300 Subject: [PATCH 5/8] style: Fix type hints and type casting --- .editorconfig | 4 ---- mailjet_rest/client.py | 42 ++++++++++++++++++++------------------- pyproject.toml | 2 +- tests/unit/test_client.py | 40 ++++++++----------------------------- 4 files changed, 31 insertions(+), 57 deletions(-) diff --git a/.editorconfig b/.editorconfig index 07dc7ba..9ca629b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,10 +13,6 @@ end_of_line = lf [*.md] trim_trailing_whitespace = false -[*.bat] -indent_style = tab -end_of_line = crlf - [LICENSE] insert_final_newline = false diff --git a/mailjet_rest/client.py b/mailjet_rest/client.py index 5d2254d..9a2cacb 100644 --- a/mailjet_rest/client.py +++ b/mailjet_rest/client.py @@ -120,19 +120,6 @@ class ApiRateLimitError(ApiError): # Utilities # ========================================== - -def prepare_url(match: Any) -> str: - """Replace capital letters in the input string with a dash prefix and convert to lowercase. - - Args: - match (Any): A regex match object containing a capital letter. - - Returns: - str: A formatted URL string fragment (e.g., '_m'). - """ - return f"_{match.group(0).lower()}" - - # --- Deprecated Utilities --- @@ -152,15 +139,15 @@ def logging_handler(to_file: bool = False, **_kwargs: Any) -> logging.Logger: # ) warnings.warn(msg, DeprecationWarning, stacklevel=2) - logger = logging.getLogger("mailjet_legacy") - logger.setLevel(logging.DEBUG) + legacy_logger = logging.getLogger("mailjet_legacy") + legacy_logger.setLevel(logging.DEBUG) formatter = logging.Formatter("%(levelname)s | %(message)s") stdout_handler = logging.StreamHandler(sys.stdout) stdout_handler.setFormatter(formatter) - logger.addHandler(stdout_handler) + legacy_logger.addHandler(stdout_handler) # Return a safe, isolated logger so downstream code like `logger.debug()` doesn't crash - return logger + return legacy_logger def parse_response( @@ -194,7 +181,8 @@ def parse_response( # Soft legacy support: run the logger if explicitly passed without crashing if debug and callable(log): with suppress(Exception): - lgr = log() + lgr = cast("logging.Logger", cast("object", log())) + lgr.debug("REQUEST: %s", response.request.url) lgr.debug("RESPONSE_CODE: %s", response.status_code) logging.getLogger().handlers.clear() @@ -303,8 +291,18 @@ class Endpoint: def __post_init__(self) -> None: """Pre-compute routing strings ONCE instead of on every network call.""" self._name_lower = self.name.lower() - self._action_parts = self.name.split("_") - self._resource_lower = self._action_parts[0].lower() + parts = self.name.split("_") + + # Base resource ignores CamelCase-to-dash conversion (matches legacy behavior) + self._resource_lower = parts[0].lower() + self._action_parts = [self._resource_lower] + + # Re-implement camelCase-to-dash conversion natively for sub-actions + if len(parts) > 1: + for part in parts[1:]: + # Convert 'linkClick' to 'link-click' natively + dashed = "".join("-" + c.lower() if c.isupper() else c for c in part) + self._action_parts.append(dashed.lstrip("-")) @staticmethod def _build_csv_url(base_url: str, version: str, resource: str, name_lower: str, id_val: int | str | None) -> str: @@ -621,6 +619,10 @@ class Client: "myprofile", ) + config: Config + session: requests.Session + _endpoint_cache: dict[str, Endpoint] + # --- Initialization & Magic Methods --- def __init__( diff --git a/pyproject.toml b/pyproject.toml index 2dd5a14..a171634 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -252,7 +252,7 @@ namespace_packages = true pretty = true # 3rd party import ignore_missing_imports = true -# flag to suppress Name already defined on line +# flag to suppress Name already defined on a line allow_redefinition = false # Disallow dynamic typing disallow_any_unimported = false diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 2585d33..e50fde7 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -13,17 +13,15 @@ from requests.exceptions import RequestException from requests.exceptions import Timeout as RequestsTimeout -from mailjet_rest._version import __version__ from mailjet_rest.client import ( ApiError, Client, Config, CriticalApiError, TimeoutError, - prepare_url, ) from mailjet_rest.utils.guardrails import SecurityGuard -from mailjet_rest.client import _JSON_HEADERS, _TEXT_HEADERS +from mailjet_rest.client import _JSON_HEADERS, _TEXT_HEADERS # type: ignore[attr-defined] if TYPE_CHECKING: # Explicitly import fixture type for MyPy in a type-checking block @@ -253,6 +251,12 @@ def test_statcounters_endpoint_routing(client_offline: Client) -> None: assert url == "https://api.mailjet.com/v3/REST/statcounters" +def test_camel_case_to_dash_routing(client_offline: Client) -> None: + """Verify that CamelCase endpoints correctly translate to dashed paths (e.g., linkClick -> link-click).""" + url = client_offline.statistics_linkClick._build_url() + assert "link-click" in url, f"Expected 'link-click' in URL, got {url}" + + # ========================================== # 4. HTTP Execution & Network Handling Tests # ========================================== @@ -413,34 +417,6 @@ def mock_request(method: str, url: str, **kwargs: Any) -> requests.Response: # Calling with action_id but no id client_offline.contact.get(action_id=123) - -def test_prepare_url_headers_and_url() -> None: - assert prepare_url(re.search(r"[A-Z]", "MyURL")) == "_m" - - -def test_prepare_url_mixed_case_input() -> None: - match = re.search(r"[A-Z]", "mixedCaseInput") - assert match is not None - assert prepare_url(match) == "_c" - - -def test_prepare_url_empty_input() -> None: - match = re.search(r"[A-Z]", "") - assert match is None - - -def test_prepare_url_with_numbers_input_bad() -> None: - match = re.search(r"[A-Z]", "url1With2Numbers") - assert match is not None - assert prepare_url(match) == "_w" - - -def test_prepare_url_leading_trailing_underscores_input_bad() -> None: - match = re.search(r"[A-Z]", "_urlWithUnderscores_") - assert match is not None - assert prepare_url(match) == "_w" - - # ========================================== # 5. Resource Management (Context Managers) # ========================================== @@ -534,7 +510,7 @@ def test_endpoint_precomputes_routing_strings(client_offline: Client) -> None: endpoint = getattr(client_offline, "Contact_Data") assert getattr(endpoint, "_name_lower") == "contact_data" - assert getattr(endpoint, "_action_parts") == ["Contact", "Data"] + assert getattr(endpoint, "_action_parts") == ["contact", "data"] assert getattr(endpoint, "_resource_lower") == "contact" From bc6adf9b4b12e95e8ef473a362d0ff0a479c3c1f Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:12:19 +0300 Subject: [PATCH 6/8] build(conda.recipe): Update the recipe --- conda.recipe/meta.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 8567d1a..48aa540 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -30,9 +30,8 @@ requirements: {% endfor %} run: - python - {% for dep in pyproject['project']['dependencies'] %} - - {{ dep.lower() }} - {% endfor %} + - requests >=2.33.0 + - typing-extensions >=4.7.1 # [py<311] test: imports: From ad0b94f1f2e1d70cb5bab3197759dac6a1f26528 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:51:25 +0300 Subject: [PATCH 7/8] build: Update version to 1.6.0 --- mailjet_rest/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mailjet_rest/_version.py b/mailjet_rest/_version.py index a91f75c..df44d33 100644 --- a/mailjet_rest/_version.py +++ b/mailjet_rest/_version.py @@ -1 +1 @@ -__version__ = "1.6.0rc1" \ No newline at end of file +__version__ = "1.6.0" \ No newline at end of file From e65e381ce817b07793851458faaacb273c9e9277 Mon Sep 17 00:00:00 2001 From: Serhii Kupriienko <61395455+skupriienko@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:56:54 +0300 Subject: [PATCH 8/8] docs: Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fee388b..a363f4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ We [keep a changelog.](http://keepachangelog.com/) ## [Unreleased] -## [1.6.0] - 2026-04-XX +## [1.6.0] - 2026-04-27 ### Security