Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
76924dd
build(dependency): replace an obsolete httpx dependency with a modern…
skupriienko-mailgun Oct 8, 2026
3a265fb
chore(lint): configure Ruff per-file-ignores for test suites
skupriienko-mailgun Oct 8, 2026
59f8403
test(unit): apply Ruff formatting and linter fixes across unit tests
skupriienko-mailgun Oct 8, 2026
0d658ce
test(fuzz): format fuzz targets and resolve linter violations
skupriienko-mailgun Oct 8, 2026
1768bea
test(integration): format integration, property, and regression suites
skupriienko-mailgun Oct 8, 2026
48e918c
test: update test_post_query_get_account_logs to guard the item key v…
skupriienko-mailgun Oct 8, 2026
a365e23
docs(release): update security policy
skupriienko-mailgun Oct 8, 2026
5b3a284
fix(endpoints): harden stream pagination against cyclic cursors and a…
skupriienko-mailgun Oct 8, 2026
35bcb5f
test(sdk): update tag metadata to httpx2 and expand fuzzer dictionary
skupriienko-mailgun Oct 8, 2026
1a2ab38
docs: document stream pagination fixes and finalize v1.10.0 release n…
skupriienko-mailgun Oct 8, 2026
1a9b169
Potential fix for pull request finding 'Empty except'
skupriienko-mailgun Oct 8, 2026
2761418
fix(endpoints): prevent retries on non-transient client and protocol …
skupriienko-mailgun Oct 9, 2026
205135b
test(fuzz): disable sleep latency and enforce no-retry policy in audi…
skupriienko-mailgun Oct 9, 2026
e05addc
docs(changelog): document unrecoverable error handling and fuzz test …
skupriienko-mailgun Oct 9, 2026
e93bc4c
Potential fix for pull request finding 'Empty except'
skupriienko-mailgun Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ repos:
name: "🔒 security · Detect private keys"

- repo: https://github.com/commitizen-tools/commitizen
rev: v4.18.1
rev: v4.19.2
hooks:
- id: commitizen
name: "🌳 git · Validate commit message"
Expand Down Expand Up @@ -134,7 +134,7 @@ repos:
additional_dependencies: [".[toml]"]

- repo: https://github.com/semgrep/pre-commit
rev: 'v1.177.0'
rev: 'v1.180.0'
hooks:
- id: semgrep
name: "🔒 security · Static analysis (semgrep)"
Expand All @@ -143,14 +143,14 @@ repos:

# Spelling and typos
- repo: https://github.com/crate-ci/typos
rev: v1.50.2
rev: v1.51.1
hooks:
- id: typos
name: "📝 spelling · Check typos"

# CI/CD validation
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.38.0
rev: 0.38.2
hooks:
- id: check-dependabot
name: "🔧 ci/cd · Validate Dependabot config"
Expand All @@ -172,7 +172,7 @@ repos:
- responses

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.8
rev: v0.16.10
hooks:
- id: ruff-check
name: "🐍 lint · Check with Ruff"
Expand All @@ -191,7 +191,7 @@ repos:

# Python type checking
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v2.3.1
rev: v2.4.0
hooks:
- id: mypy
name: "🐍 types · Check with mypy"
Expand Down
30 changes: 29 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,33 @@ We [keep a changelog.](http://keepachangelog.com/)

## [Unreleased]

## [1.10.0] - 2026-10-08

### Changed

- **Async Engine Migration (`httpx2`):** Migrated the asynchronous engine exclusively to `httpx2 >=2.7.0`, removing `httpx >=0.24.0` from runtime dependencies across `pyproject.toml`, `environment.yaml`, and `environment-dev.yaml`.
- **Core HTTP Import Refactoring:** Updated `mailgun.client`, `mailgun.endpoints`, `mailgun.security`, and `mailgun.types` to interface directly with `httpx2` for client instances (`httpx2.AsyncClient`), requests (`httpx2.Request`), responses (`httpx2.Response`), timeouts (`httpx2.Timeout`), and network exceptions (`httpx2.RequestError`, `httpx2.TimeoutException`, `httpx2.ConnectError`, `httpx2.NetworkError`, `httpx2.HTTPStatusError`).
- **Tooling & Static Analysis Alignment:** Synchronized pre-commit hooks (`commitizen v4.19.2`, `semgrep v1.180.0`, `typos v1.51.1`, `check-jsonschema 0.38.2`, `ruff v0.16.10`, and `mypy v2.4.0`). Added `COM812` (`missing-trailing-comma`) to `[tool.ruff.lint.ignore]` to eliminate Ruff formatter conflicts, and configured test-suite rule relaxations under `[tool.ruff.lint.per-file-ignores]` for `tests/**`.
- **Security Policy Update:** Updated `SECURITY.md` supported versions table to mark `1.10.x` supported and deprecate `< 1.10.0`.
- **Fuzzing Harness Latency & Retry Optimization:** Mocked `time.sleep` in `tests/fuzz/fuzz_audit_events.py` to eliminate artificial delays during fuzz test runs, passed a zero-retry policy configuration (`_NO_RETRY_CONFIG`) to the client, and safely handled expected `ValueError` exceptions when testing hostile URLs via `SecurityGuard.validate_mailgun_url`.

### Removed

- **`_httpx_compat` Bridge Layer:** Removed `mailgun/_httpx_compat.py` and its runtime detection flag (`HAS_HTTPX2`), eliminating dual-dependency branching and legacy import redirection.
- **Obsolete Compat Tests:** Removed `tests/unit/test_httpx_compat.py`.

### Fixed

- **Stream Pagination Hardening:** Added duplicate and stagnant cursor detection (`prev_url`) in both `Endpoint.stream()` and `AsyncEndpoint.stream()` to prevent infinite loops. Wrapped synchronous `requests.HTTPError` in `ApiError` for contract parity with the async client, and eliminated redundant loop termination checks.
- **Retry Short-Circuiting on Unrecoverable Client Errors:** Suppressed retry attempts in `Endpoint` and `AsyncEndpoint` for permanent client-side exceptions. Configuration errors (`InvalidURL`, `InvalidHeader`, `InvalidSchema`, `MissingSchema`) and protocol/decoding errors (`UnsupportedProtocol`, `DecodingError`) are now dispatched directly to `_handle_api_error` without backoff delays.
- **Telemetry Log Item Index Guard:** Added an `items` existence check in `test_post_query_get_account_logs` across `test_integration_async.py` and `test_integration_sync.py` to prevent `IndexError` when querying accounts with empty log retention windows.
- **Test Suite Quality & Formatting:** Corrected verbose exception re-raising in `test_integration_coverage.py`, added missing placeholder formatting to `sdk_logger.info` calls, and formatted all unit, integration, and fuzz test targets using `ruff format`.

### Pull Requests Merged

- PR #68: build(deps): Bump the minor-and-patch group with 2 updates.
- PR #70: build(deps): Bump the minor-and-patch group with 2 updates.

## [1.9.1] - 2026-09-21

### Security
Expand Down Expand Up @@ -505,6 +532,7 @@ We [keep a changelog.](http://keepachangelog.com/)
[1.0.1]: https://github.com/mailgun/mailgun-python/releases/tag/v1.0.1
[1.0.2]: https://github.com/mailgun/mailgun-python/releases/tag/v1.0.2
[1.1.0]: https://github.com/mailgun/mailgun-python/releases/tag/v1.1.0
[1.10.0]: https://github.com/mailgun/mailgun-python/releases/tag/v1.10.0
[1.2.0]: https://github.com/mailgun/mailgun-python/releases/tag/v1.2.0
[1.3.0]: https://github.com/mailgun/mailgun-python/releases/tag/v1.3.0
[1.4.0]: https://github.com/mailgun/mailgun-python/releases/tag/v1.4.0
Expand All @@ -515,4 +543,4 @@ We [keep a changelog.](http://keepachangelog.com/)
[1.8.0]: https://github.com/mailgun/mailgun-python/releases/tag/v1.8.0
[1.9.0]: https://github.com/mailgun/mailgun-python/releases/tag/v1.9.0
[1.9.1]: https://github.com/mailgun/mailgun-python/releases/tag/v1.9.1
[unreleased]: https://github.com/mailgun/mailgun-python/compare/v1.9.1...HEAD
[unreleased]: https://github.com/mailgun/mailgun-python/compare/v1.10.0...HEAD
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,7 @@ To build the `mailgun` package from the sources you need `setuptools` (as a buil

### Runtime dependencies

At runtime the package requires `requests >=2.33.0`. For async support, it uses `httpx2 >=2.7.0`.
Async client automatically detects and uses `httpx2` if available, falling back seamlessly to legacy `httpx`.
At runtime the package requires `requests >=2.33.0`. For async support, it requires `httpx2 >=2.7.0`.

### Test dependencies

Expand Down Expand Up @@ -675,7 +674,7 @@ data = {
"to": ["recipient1@example.com", "recipient2@example.com"],
"subject": "Advanced Mailgun Features",
"text": "Testing out tags, custom variables, and testmode!",
"o:tag": ["newsletter", "python-sdk"], # Multiple tags
"o:tag": ["async-integration-test", "httpx2-sdk"], # Multiple tags
"o:testmode": "yes", # Validates payload without actually sending
"o:deliverytime-optimize-period": "24h", # Send Time Optimization
"v:my-custom-id": "USER-12345", # Custom user-defined variable
Expand Down
8 changes: 4 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

## Supported Versions

| Version | Supported |
| ------- | ------------------ |
| 1.9.x | :white_check_mark: |
| < 1.9.0 | :x: |
| Version | Supported |
| -------- | ------------------ |
| 1.10.x | :white_check_mark: |
| < 1.10.0 | :x: |

# Vulnerability Disclosure

Expand Down
1 change: 0 additions & 1 deletion environment-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ dependencies:
# runtime deps
- requests >=2.33.0
- httpx2 >=2.7.0
- httpx >=0.24.0
# extras
- fastapi
- pydantic >=2.0.0
Expand Down
1 change: 0 additions & 1 deletion environment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ dependencies:
# runtime deps
- requests >=2.33.0
- httpx2 >=2.7.0
- httpx >=0.24.0
# tests
- pytest >=9.0.3
# other
Expand Down
28 changes: 0 additions & 28 deletions mailgun/_httpx_compat.py

This file was deleted.

22 changes: 11 additions & 11 deletions mailgun/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
from http import HTTPStatus
from typing import TYPE_CHECKING, Any, Final, Self

import httpx2
import requests # pyright: ignore[reportMissingModuleSource]

from mailgun._httpx_compat import httpx
from mailgun.config import Config
from mailgun.endpoints import AsyncEndpoint, BaseEndpoint, Endpoint
from mailgun.filters import RedactingFilter
Expand Down Expand Up @@ -282,7 +282,7 @@ def ping(self) -> bool:


class AsyncClient(BaseClient):
"""Async client class using httpx."""
"""Async client class using httpx2."""

__slots__ = ("_client_kwargs", "_httpx_client")

Expand All @@ -301,7 +301,7 @@ def __init__(
"""
super().__init__(auth=auth, **kwargs)
self._client_kwargs = kwargs.get("client_kwargs", {})
self._httpx_client: httpx.AsyncClient | None = None
self._httpx_client: httpx2.AsyncClient | None = None

def __getattr__(self, name: str) -> Any:
"""Resolve and return the requested API endpoint instance.
Expand Down Expand Up @@ -341,14 +341,14 @@ def __getattr__(self, name: str) -> Any:
return endpoint

@property
def _client(self) -> httpx.AsyncClient:
"""Provide lazy initialization for the underlying httpx.AsyncClient.
def _client(self) -> httpx2.AsyncClient:
"""Provide lazy initialization for the underlying httpx2.AsyncClient.

Returns:
The active httpx.AsyncClient instance.
The active httpx2.AsyncClient instance.
"""
# Assign to a local variable so Pyright can properly narrow the type
current_client: httpx.AsyncClient | None = getattr(self, "_httpx_client", None)
current_client: httpx2.AsyncClient | None = getattr(self, "_httpx_client", None)

if current_client is None or current_client.is_closed:
# Enforce TLS 1.2+ for httpx (CWE-319)
Expand All @@ -358,20 +358,20 @@ def _client(self) -> httpx.AsyncClient:
# Check if the user already provided a custom transport (e.g. for mocking)
kwargs = self._client_kwargs.copy()
if "transport" not in kwargs:
limits = httpx.Limits(max_keepalive_connections=100, max_connections=100)
kwargs["transport"] = httpx.AsyncHTTPTransport(
limits = httpx2.Limits(max_keepalive_connections=100, max_connections=100)
kwargs["transport"] = httpx2.AsyncHTTPTransport(
retries=3,
limits=limits,
verify=ssl_context,
)

self._httpx_client = httpx.AsyncClient(**kwargs)
self._httpx_client = httpx2.AsyncClient(**kwargs)
return self._httpx_client

return current_client

async def aclose(self) -> None:
"""Close the underlying httpx.AsyncClient and purge memory."""
"""Close the underlying httpx2.AsyncClient and purge memory."""
if self._httpx_client:
try:
# CWE-316: Clear async session
Expand Down
Loading
Loading