Skip to content

Commit 2bce502

Browse files
authored
Merge pull request #128 from mailjet/release/1.6.0
Release 1.6.0
2 parents 1ea053d + e65e381 commit 2bce502

10 files changed

Lines changed: 48 additions & 69 deletions

File tree

.editorconfig

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,6 @@ end_of_line = lf
1313
[*.md]
1414
trim_trailing_whitespace = false
1515

16-
[*.bat]
17-
indent_style = tab
18-
end_of_line = crlf
19-
2016
[LICENSE]
2117
insert_final_newline = false
2218

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ We [keep a changelog.](http://keepachangelog.com/)
44

55
## [Unreleased]
66

7+
## [1.6.0] - 2026-04-27
8+
79
### Security
810

911
- **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,9 @@ We [keep a changelog.](http://keepachangelog.com/)
6870
### Pull Requests Merged
6971

7072
- [PR_125](https://github.com/mailjet/mailjet-apiv3-python/pull/125) - Refactor client.
73+
- [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
74+
- [PR_128](https://github.com/mailjet/mailjet-apiv3-python/pull/128) - Release 1.6.0.
75+
- [PR_129](https://github.com/mailjet/mailjet-apiv3-python/pull/129) - Use hyphen in the package name in readme.
7176

7277
## [1.5.1] - 2025-07-14
7378

@@ -255,4 +260,5 @@ We [keep a changelog.](http://keepachangelog.com/)
255260
[1.4.0]: https://github.com/mailjet/mailjet-apiv3-python/releases/tag/v1.4.0
256261
[1.5.0]: https://github.com/mailjet/mailjet-apiv3-python/releases/tag/v1.5.0
257262
[1.5.1]: https://github.com/mailjet/mailjet-apiv3-python/releases/tag/v1.5.1
258-
[unreleased]: https://github.com/mailjet/mailjet-apiv3-python/releases/tag/v1.5.1...HEAD
263+
[1.6.0]: https://github.com/mailjet/mailjet-apiv3-python/releases/tag/v1.6.0
264+
[unreleased]: https://github.com/mailjet/mailjet-apiv3-python/releases/tag/v1.6.0...HEAD

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ with Client(auth=(api_key, api_secret), version="v3.1") as mailjet:
189189
(Note:
190190

191191
> **Note**
192-
> If you choose not to use the context manager, you should manually call mailjet.close() when your application shuts down).
192+
> If you choose not to use the context manager, you should manually call mailjet.close() when your application shuts down.
193193
194194
### Advanced Configuration
195195

@@ -464,7 +464,7 @@ Some requests (for example [GET /contact](https://dev.mailjet.com/email/referenc
464464
`limit` `int` Limit the response to a select number of returned objects. Default value: `10`. Maximum value: `1000`
465465
`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`
466466
`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`
467-
Next example returns 40 contacts starting from 51th record sorted by `Email` field descendally:
467+
Next example returns 40 contacts starting from 51st record sorted by `Email` field descendally:
468468

469469
```python
470470
filters = {
@@ -645,7 +645,7 @@ Feel free to ask anything, and contribute:
645645
- Create a new branch.
646646
- Implement your feature or bug fix.
647647
- Add documentation to it.
648-
- Commit, push, open a pull request and voila.
648+
- Commit, push, open a pull request and voilà.
649649

650650
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).
651651

SECURITY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Please include the following details:
2626

2727
If English is not your first language, please try to describe the
2828
problem and its impact to the best of your ability. For greater detail,
29-
please use your native language and we will try our best to translate it
29+
please use your native language, and we will try our best to translate it
3030
using online services.
3131

3232
Please also include the code you used to find the problem and the

conda.recipe/meta.yaml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,8 @@ requirements:
3030
{% endfor %}
3131
run:
3232
- python
33-
{% for dep in pyproject['project']['dependencies'] %}
34-
- {{ dep.lower() }}
35-
{% endfor %}
33+
- requests >=2.33.0
34+
- typing-extensions >=4.7.1 # [py<311]
3635

3736
test:
3837
imports:

mailjet_rest/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.5.1.post1.dev40"
1+
__version__ = "1.6.0"

mailjet_rest/client.py

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -120,19 +120,6 @@ class ApiRateLimitError(ApiError):
120120
# Utilities
121121
# ==========================================
122122

123-
124-
def prepare_url(match: Any) -> str:
125-
"""Replace capital letters in the input string with a dash prefix and convert to lowercase.
126-
127-
Args:
128-
match (Any): A regex match object containing a capital letter.
129-
130-
Returns:
131-
str: A formatted URL string fragment (e.g., '_m').
132-
"""
133-
return f"_{match.group(0).lower()}"
134-
135-
136123
# --- Deprecated Utilities ---
137124

138125

@@ -141,7 +128,7 @@ def logging_handler(to_file: bool = False, **_kwargs: Any) -> logging.Logger: #
141128
142129
Args:
143130
to_file (bool): Deprecated flag. Output is no longer written to files natively.
144-
**kwargs (Any): Absorbs any other legacy keyword arguments.
131+
**_kwargs (Any): Absorbs any other legacy keyword arguments.
145132
146133
Returns:
147134
logging.Logger: A legacy logger instance to prevent AttributeError in old integrations.
@@ -152,15 +139,15 @@ def logging_handler(to_file: bool = False, **_kwargs: Any) -> logging.Logger: #
152139
)
153140
warnings.warn(msg, DeprecationWarning, stacklevel=2)
154141

155-
logger = logging.getLogger("mailjet_legacy")
156-
logger.setLevel(logging.DEBUG)
142+
legacy_logger = logging.getLogger("mailjet_legacy")
143+
legacy_logger.setLevel(logging.DEBUG)
157144
formatter = logging.Formatter("%(levelname)s | %(message)s")
158145
stdout_handler = logging.StreamHandler(sys.stdout)
159146
stdout_handler.setFormatter(formatter)
160-
logger.addHandler(stdout_handler)
147+
legacy_logger.addHandler(stdout_handler)
161148

162149
# Return a safe, isolated logger so downstream code like `logger.debug()` doesn't crash
163-
return logger
150+
return legacy_logger
164151

165152

166153
def parse_response(
@@ -175,7 +162,7 @@ def parse_response(
175162
response (requests.Response): The HTTP response.
176163
log (Any, optional): Deprecated logging callable.
177164
debug (bool): Deprecated debug flag.
178-
**kwargs (Any): Absorbs any other legacy keyword arguments.
165+
**_kwargs (Any): Absorbs any other legacy keyword arguments.
179166
180167
Returns:
181168
Any: The parsed JSON dictionary or raw text string.
@@ -194,7 +181,8 @@ def parse_response(
194181
# Soft legacy support: run the logger if explicitly passed without crashing
195182
if debug and callable(log):
196183
with suppress(Exception):
197-
lgr = log()
184+
lgr = cast("logging.Logger", cast("object", log()))
185+
198186
lgr.debug("REQUEST: %s", response.request.url)
199187
lgr.debug("RESPONSE_CODE: %s", response.status_code)
200188
logging.getLogger().handlers.clear()
@@ -303,8 +291,18 @@ class Endpoint:
303291
def __post_init__(self) -> None:
304292
"""Pre-compute routing strings ONCE instead of on every network call."""
305293
self._name_lower = self.name.lower()
306-
self._action_parts = self.name.split("_")
307-
self._resource_lower = self._action_parts[0].lower()
294+
parts = self.name.split("_")
295+
296+
# Base resource ignores CamelCase-to-dash conversion (matches legacy behavior)
297+
self._resource_lower = parts[0].lower()
298+
self._action_parts = [self._resource_lower]
299+
300+
# Re-implement camelCase-to-dash conversion natively for sub-actions
301+
if len(parts) > 1:
302+
for part in parts[1:]:
303+
# Convert 'linkClick' to 'link-click' natively
304+
dashed = "".join("-" + c.lower() if c.isupper() else c for c in part)
305+
self._action_parts.append(dashed.lstrip("-"))
308306

309307
@staticmethod
310308
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:
621619
"myprofile",
622620
)
623621

622+
config: Config
623+
session: requests.Session
624+
_endpoint_cache: dict[str, Endpoint]
625+
624626
# --- Initialization & Magic Methods ---
625627

626628
def __init__(

mailjet_rest/utils/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def clean_version(version_str: str) -> tuple[int, ...]:
3434
except (IndexError, ValueError):
3535
return 0, 0, 0
3636
else:
37-
return (major, minor, patch)
37+
return major, minor, patch
3838

3939

4040
# VERSION is a tuple of integers (1, 3, 2).

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ namespace_packages = true
252252
pretty = true
253253
# 3rd party import
254254
ignore_missing_imports = true
255-
# flag to suppress Name <var> already defined on line
255+
# flag to suppress Name <var> already defined on a line
256256
allow_redefinition = false
257257
# Disallow dynamic typing
258258
disallow_any_unimported = false

tests/unit/test_client.py

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,15 @@
1313
from requests.exceptions import RequestException
1414
from requests.exceptions import Timeout as RequestsTimeout
1515

16-
from mailjet_rest._version import __version__
1716
from mailjet_rest.client import (
1817
ApiError,
1918
Client,
2019
Config,
2120
CriticalApiError,
2221
TimeoutError,
23-
prepare_url,
2422
)
2523
from mailjet_rest.utils.guardrails import SecurityGuard
26-
from mailjet_rest.client import _JSON_HEADERS, _TEXT_HEADERS
24+
from mailjet_rest.client import _JSON_HEADERS, _TEXT_HEADERS # type: ignore[attr-defined]
2725

2826
if TYPE_CHECKING:
2927
# Explicitly import fixture type for MyPy in a type-checking block
@@ -253,6 +251,12 @@ def test_statcounters_endpoint_routing(client_offline: Client) -> None:
253251
assert url == "https://api.mailjet.com/v3/REST/statcounters"
254252

255253

254+
def test_camel_case_to_dash_routing(client_offline: Client) -> None:
255+
"""Verify that CamelCase endpoints correctly translate to dashed paths (e.g., linkClick -> link-click)."""
256+
url = client_offline.statistics_linkClick._build_url()
257+
assert "link-click" in url, f"Expected 'link-click' in URL, got {url}"
258+
259+
256260
# ==========================================
257261
# 4. HTTP Execution & Network Handling Tests
258262
# ==========================================
@@ -413,34 +417,6 @@ def mock_request(method: str, url: str, **kwargs: Any) -> requests.Response:
413417
# Calling with action_id but no id
414418
client_offline.contact.get(action_id=123)
415419

416-
417-
def test_prepare_url_headers_and_url() -> None:
418-
assert prepare_url(re.search(r"[A-Z]", "MyURL")) == "_m"
419-
420-
421-
def test_prepare_url_mixed_case_input() -> None:
422-
match = re.search(r"[A-Z]", "mixedCaseInput")
423-
assert match is not None
424-
assert prepare_url(match) == "_c"
425-
426-
427-
def test_prepare_url_empty_input() -> None:
428-
match = re.search(r"[A-Z]", "")
429-
assert match is None
430-
431-
432-
def test_prepare_url_with_numbers_input_bad() -> None:
433-
match = re.search(r"[A-Z]", "url1With2Numbers")
434-
assert match is not None
435-
assert prepare_url(match) == "_w"
436-
437-
438-
def test_prepare_url_leading_trailing_underscores_input_bad() -> None:
439-
match = re.search(r"[A-Z]", "_urlWithUnderscores_")
440-
assert match is not None
441-
assert prepare_url(match) == "_w"
442-
443-
444420
# ==========================================
445421
# 5. Resource Management (Context Managers)
446422
# ==========================================
@@ -534,7 +510,7 @@ def test_endpoint_precomputes_routing_strings(client_offline: Client) -> None:
534510
endpoint = getattr(client_offline, "Contact_Data")
535511

536512
assert getattr(endpoint, "_name_lower") == "contact_data"
537-
assert getattr(endpoint, "_action_parts") == ["Contact", "Data"]
513+
assert getattr(endpoint, "_action_parts") == ["contact", "data"]
538514
assert getattr(endpoint, "_resource_lower") == "contact"
539515

540516

0 commit comments

Comments
 (0)