Skip to content

Commit 3f425fb

Browse files
authored
Merge pull request #36 from mailgun/improments
Improve client, update & fix tests
2 parents 2ab0add + 4f897be commit 3f425fb

57 files changed

Lines changed: 5155 additions & 2896 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.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

.github/dependabot.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,6 @@ updates:
2020
directory: '/'
2121
schedule:
2222
interval: 'weekly'
23+
groups:
24+
minor-and-patch:
25+
update-types: [ "minor", "patch" ]

.github/workflows/commit_checks.yaml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ name: CI
33

44
on:
55
push:
6-
branches:
7-
- main
6+
branches: [main]
87
pull_request:
8+
branches: [main]
99

1010
permissions:
1111
contents: read
@@ -30,8 +30,7 @@ jobs:
3030
fail-fast: false
3131
matrix:
3232
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
33-
# TODO: Enable Python 3.14 when conda and conda-build will have py314 support.
34-
python-version: ["3.10", "3.11", "3.12", "3.13"]
33+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
3534
env:
3635
APIKEY: ${{ secrets.APIKEY }}
3736
DOMAIN: ${{ secrets.DOMAIN }}
@@ -46,9 +45,11 @@ jobs:
4645
channels: defaults
4746
show-channel-urls: true
4847
environment-file: environment-dev.yaml
48+
cache: 'pip' # Drastically speeds up CI by caching pip dependencies
4949

50-
- name: Install the package
50+
- name: Install package
5151
run: |
52+
python -m pip install --upgrade pip
5253
pip install .
5354
conda info
5455
@@ -60,5 +61,5 @@ jobs:
6061
python -m pip install --upgrade pip
6162
pip install pytest
6263
63-
- name: Tests
64+
- name: Run Unit Tests
6465
run: pytest -v tests/unit/

.github/workflows/issue-triage.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
issues: write
1515
steps:
1616
- name: Initial triage
17-
uses: actions/github-script@v8
17+
uses: actions/github-script@v9
1818
with:
1919
github-token: ${{ secrets.GITHUB_TOKEN }}
2020
script: |

.github/workflows/pr_validation.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ jobs:
2222

2323
- name: Build package
2424
run: |
25-
pip install --upgrade build setuptools wheel setuptools-scm
25+
pip install --upgrade build setuptools wheel setuptools-scm twine
2626
python -m build
27+
twine check dist/*
2728
2829
- name: Test installation
2930
run: |
31+
# Install the built wheel to ensure packaging didn't miss files
3032
pip install dist/*.whl
31-
python -c "from importlib.metadata import version; print(version('mailgun'))"
33+
python -c "import mailgun; from importlib.metadata import version; print(f'Successfully installed v{version(\"mailgun\")}')"

.github/workflows/publish.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ permissions:
1212

1313
jobs:
1414
publish:
15+
name: Build and Publish to PyPI
1516
runs-on: ubuntu-latest
1617
permissions:
1718
contents: read
@@ -31,14 +32,18 @@ jobs:
3132

3233
- name: Extract version
3334
id: get_version
35+
env:
36+
EVENT_NAME: ${{ github.event_name }}
37+
RELEASE_TAG: ${{ github.event.release.tag_name }}
38+
REF_NAME: ${{ github.ref_name }}
3439
run: |
3540
# Get clean version from the tag or release
36-
if [[ "${{ github.event_name }}" == "release" ]]; then
41+
if [[ "$EVENT_NAME" == "release" ]]; then
3742
# For releases, get the version from the release tag
38-
TAG_NAME="${{ github.event.release.tag_name }}"
43+
TAG_NAME="$RELEASE_TAG"
3944
else
4045
# For tags, get version from the tag
41-
TAG_NAME="${{ github.ref_name }}"
46+
TAG_NAME="$REF_NAME"
4247
fi
4348
4449
# Remove 'v' prefix

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,3 +305,7 @@ pytestdebug.log
305305

306306
# local temp files
307307
.server.key
308+
309+
310+
# Benchmarking
311+
.benchmarks/

.pre-commit-config.yaml

Lines changed: 25 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,8 @@ repos:
101101
- id: detect-private-key
102102
name: "🔒 security · Detect private keys"
103103

104-
# Git commit quality
105-
- repo: https://github.com/jorisroovers/gitlint
106-
rev: v0.19.1
107-
hooks:
108-
- id: gitlint
109-
name: "🌳 git · Validate commit format"
110-
111104
- repo: https://github.com/commitizen-tools/commitizen
112-
rev: v4.13.9
105+
rev: v4.13.10
113106
hooks:
114107
- id: commitizen
115108
name: "🌳 git · Validate commit message"
@@ -123,7 +116,7 @@ repos:
123116
name: "🔒 security · Detect committed secrets"
124117

125118
- repo: https://github.com/gitleaks/gitleaks
126-
rev: v8.30.1
119+
rev: v8.30.0
127120
hooks:
128121
- id: gitleaks
129122
name: "🔒 security · Scan for hardcoded secrets"
@@ -146,7 +139,7 @@ repos:
146139
# args: ['--desc', 'on']
147140

148141
- repo: https://github.com/semgrep/pre-commit
149-
rev: 'v1.156.0'
142+
rev: 'v1.159.0'
150143
hooks:
151144
- id: semgrep
152145
name: "🔒 security · Static analysis (semgrep)"
@@ -155,7 +148,7 @@ repos:
155148

156149
# Spelling and typos
157150
- repo: https://github.com/crate-ci/typos
158-
rev: v1.44.0
151+
rev: v1.45.2
159152
hooks:
160153
- id: typos
161154
name: "📝 spelling · Check typos"
@@ -170,57 +163,26 @@ repos:
170163
name: "🔧 ci/cd · Validate GitHub workflows"
171164
files: ^\.github/workflows/.*\.ya?ml$
172165

173-
# Python code formatting (order matters: autoflake → pyupgrade → darker/ruff)
174-
- repo: https://github.com/PyCQA/autoflake
175-
rev: v2.3.3
176-
hooks:
177-
- id: autoflake
178-
name: "🐍 format · Remove unused imports"
179-
args:
180-
- --in-place
181-
- --remove-all-unused-imports
182-
- --remove-unused-variable
183-
- --ignore-init-module-imports
184-
185-
- repo: https://github.com/asottile/pyupgrade
186-
rev: v3.21.2
187-
hooks:
188-
- id: pyupgrade
189-
name: "🐍 format · Modernize syntax"
190-
args: [--py310-plus, --keep-runtime-typing]
191-
192-
- repo: https://github.com/akaihola/darker
193-
rev: v3.0.0
194-
hooks:
195-
- id: darker
196-
name: "🐍 format · Format changed lines"
197-
additional_dependencies: [black]
198-
199-
# - repo: https://github.com/astral-sh/ruff-pre-commit
200-
# rev: v0.15.6
201-
# hooks:
202-
# - id: ruff-check
203-
# name: "🐍 lint · Check with Ruff"
204-
# args: [--fix, --preview]
205-
# - id: ruff-format
206-
# name: "🐍 format · Format with Ruff"
207-
208-
# Python linting (comprehensive checks)
209-
- repo: https://github.com/pycqa/flake8
210-
rev: 7.3.0
166+
- repo: https://github.com/ariebovenberg/slotscheck
167+
rev: v0.19.1
211168
hooks:
212-
- id: flake8
213-
name: "🐍 lint · Check style (Flake8)"
214-
args: ["--ignore=E501,C901", --max-complexity=13] # Sets McCabe complexity limit
169+
- id: slotscheck
170+
name: "🔍 check · slotscheck"
215171
additional_dependencies:
216-
- radon
217-
- flake8-docstrings
218-
- Flake8-pyproject
219-
- flake8-bugbear
220-
- flake8-comprehensions
221-
- flake8-tidy-imports
222-
- pycodestyle
223-
exclude: ^tests
172+
- requests>=2.32.5
173+
- typing-extensions>=4.7.1
174+
- httpx>=0.24
175+
- pytest>=7.0.0
176+
- responses
177+
178+
- repo: https://github.com/astral-sh/ruff-pre-commit
179+
rev: v0.15.12
180+
hooks:
181+
- id: ruff-check
182+
name: "🐍 lint · Check with Ruff"
183+
args: [--fix, --preview]
184+
- id: ruff-format
185+
name: "🐍 format · Format with Ruff"
224186

225187
- repo: https://github.com/PyCQA/pylint
226188
rev: v4.0.5
@@ -230,23 +192,6 @@ repos:
230192
args:
231193
- --exit-zero
232194

233-
- repo: https://github.com/dosisod/refurb
234-
rev: v2.3.0
235-
hooks:
236-
- id: refurb
237-
name: "🐍 performance · Suggest modernizations"
238-
# TODO: Fix FURB147.
239-
args: ["--enable-all", "--ignore", "FURB147"]
240-
241-
# Python documentation
242-
- repo: https://github.com/pycqa/pydocstyle
243-
rev: 6.3.0
244-
hooks:
245-
- id: pydocstyle
246-
name: "🐍 docs · Validate docstrings"
247-
args: [--select=D200,D213,D400,D415]
248-
additional_dependencies: [tomli]
249-
250195
- repo: https://github.com/econchick/interrogate
251196
rev: 1.7.0
252197
hooks:
@@ -258,7 +203,7 @@ repos:
258203

259204
# Python type checking
260205
- repo: https://github.com/pre-commit/mirrors-mypy
261-
rev: v1.19.1
206+
rev: v1.20.2
262207
hooks:
263208
- id: mypy
264209
name: "🐍 types · Check with mypy"
@@ -269,7 +214,7 @@ repos:
269214
exclude: ^mailgun/examples/
270215

271216
- repo: https://github.com/RobertCraigie/pyright-python
272-
rev: v1.1.408
217+
rev: v1.1.409
273218
hooks:
274219
- id: pyright
275220
name: "🐍 types · Check with pyright"
@@ -305,8 +250,8 @@ repos:
305250
name: "📝 markdown · Format files"
306251
additional_dependencies:
307252
- mdformat-gfm
308-
- mdformat-black
309253
- mdformat-ruff
254+
310255
# TODO: Enable it for a single check
311256
# - repo: https://github.com/tcort/markdown-link-check
312257
# rev: v3.14.2

CHANGELOG.md

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

55
## [Unreleased]
66

7+
## [1.7.0] - 2026-04-XX
8+
79
### Added
810

11+
- Explicit `__all__` declaration in `mailgun.client` to cleanly isolate the public API namespace.
12+
- A `__repr__` method to the `Client` and `BaseEndpoint` classes to improve developer experience (DX) during console debugging (showing target routes instead of memory addresses).
13+
- Security guardrail (CWE-319) in `Config` that logs a warning if a cleartext `http://` API URL is configured.
14+
- Python 3.14 support to the GitHub Actions test matrix.
915
- Implemented Smart Logging (telemetry) in `Client` and `AsyncClient` to help users debug API requests, generated URLs, and server errors (`404`, `400`, `429`).
10-
- Added a new "Logging & Debugging" section to `README.md`.
16+
- Smart Webhook Routing: Implemented payload-based routing for domain webhooks. The SDK dynamically routes to `v1`, `v3`, or `v4` endpoints based on the HTTP method and presence of parameters like `event_types` or `url`.
17+
- Deprecation Interceptor: Added a registry and interception hook that emits non-breaking `DeprecationWarning`s and logs when utilizing obsolete Mailgun APIs (e.g., v3 validations, legacy tags, v1 bounce-classification).
1118
- Added `build_path_from_keys` utility in `mailgun.handlers.utils` to centralize and dry up URL path generation across handlers.
19+
- Overrode __dir__ in Client and AsyncClient to expose dynamic endpoint routes (e.g., .messages, .domains) directly to IDE autocompletion engines (VS Code, PyCharm).
20+
- Native dynamic routing support for Mailgun Optimize, Validations Service, and Email Preview APIs without requiring new custom handlers.
21+
- Explicit support for raw MIME string (`multipart/form-data`) uploads via the `files` parameter in the `.create()` method (essential for `client.mimemessage`).
22+
- Advanced path interpolation in `handle_default` to automatically inject inline URL parameters (e.g., `/v2/x509/{domain}/status`).
23+
- Added a new "Logging & Debugging" section to `README.md`.
24+
- An intelligent live meta-testing suite (`test_routing_meta_live.py`) to strictly verify SDK endpoint aliases against live Mailgun servers.
25+
- PEP 561 Compliance: Added a `py.typed` marker to expose the SDK's strict type hints to downstream users (`mypy`, `pyright`).
26+
- DX Tooling: Added a unified `manage.sh` script to streamline local formatting, linting, testing, and benchmarking.
27+
- Routing Engine Meta-Tests: Added `test_routing_engine.py` to dynamically validate URL generation for all 58+ supported endpoints.
1228

1329
### Changed
1430

31+
- **Memory Optimization:** Enforced `__slots__` on `Client` and `Endpoint` classes to eliminate dynamic `__dict__` overhead, reducing Garbage Collection pauses and improving overall throughput by ~8-10%.
32+
- Exception Chaining (PEP 3134): Network connection errors from `httpx` and `requests` are now explicitly chained (`raise from`), preventing the swallowing of root-cause infrastructure tracebacks.
1533
- Refactored the `Config` routing engine to use a deterministic, data-driven approach (`EXACT_ROUTES` and `PREFIX_ROUTES`) for better maintainability.
1634
- Improved dynamic API version resolution for domain endpoints to gracefully switch between `v1`, `v3`, and `v4` for nested resources, with a safe fallback to `v3`.
1735
- Secured internal configuration registries by wrapping them in `MappingProxyType` to prevent accidental mutations of the client state.
36+
- Broadened type hints for `files` (`Any | None`) and `timeout` (`int | float | tuple`) to fully support `requests`/`httpx` capabilities (like multipart lists) without triggering false positives in strict IDEs.
37+
- **Performance**: Implemented automated Payload Minification. The SDK now strips structural spaces from JSON payloads (`separators=(',', ':')`), reducing network overhead by ~15-20% for large batch requests.
38+
- **Performance**: Memoized internal route resolution logic using `@lru_cache` in `_get_cached_route_data`, eliminating redundant string splitting and dictionary lookups during repeated API calls.
39+
- Updated `DOMAIN_ENDPOINTS` mapping to reflect Mailgun's latest architecture, officially moving `tracking`, `click`, `open`, `unsubscribe`, and `webhooks` from `v1` to `v3`.
1840
- Modernized the codebase using modern Python idioms (e.g., `contextlib.suppress`) and resolved strict typing errors for `pyright`.
41+
- **Documentation**: Migrated all internal and public docstrings from legacy Sphinx/reST format to modern Google Style for cleaner readability and better IDE hover-hints.
1942
- Updated Dependabot configuration to group minor and patch updates and limit open PRs.
43+
- CI/CD Optimization: Grouped Dependabot updates (`minor-and-patch`) to reduce Pull Request noise and optimized `.editorconfig`.
44+
- Migrated the fragmented linting and formatting pipeline (Flake8, Black, Pylint, Pyupgrade, etc.) to a unified, high-performance `ruff` setup in `.pre-commit-config.yaml`.
45+
- Refactored `api_call` exception blocks to use the `else` clause for successful returns, adhering to strict Ruff (TRY300) standards.
46+
- Enabled pip dependency caching in GitHub Actions to drastically speed up CI workflows.
47+
- Fixed API versioning collisions in `DOMAIN_ENDPOINTS` (e.g., ensuring `tracking` correctly resolves to `v3` instead of `v1`).
48+
- Corrected the `credentials` route prefix to properly inject the `domains/` path segment.
49+
- Updated `README.md` with new documentation, IDE DX features, and code examples for Validations & Optimize APIs.
50+
- Cleaned up obsolete unit tests that conflicted with the new forgiving dynamic Catch-All routing architecture.
2051

2152
### Fixed
2253

54+
- Fixed a silent data loss bug in `create()` where custom `headers` passed by the user were ignored instead of being merged into the request.
55+
- Fixed a kwargs collision bug in `update()` by using `.pop("headers")` instead of `.get()` to prevent passing duplicate keyword arguments to the underlying request.
56+
- Preserved original tracebacks (PEP 3134) by properly chaining `TimeoutError` and `ApiError` using `from e`.
57+
- Used safely truncating massive HTML error responses to 500 characters (preventing a log-flooding vulnerability (OWASP CWE-532)).
58+
- Replaced a fragile `try/except TypeError` status code check with robust `getattr` and `isinstance` validation to prevent masking unrelated exceptions.
2359
- Resolved `httpx` `DeprecationWarning` in `AsyncEndpoint` by properly routing serialized JSON string payloads to the `content` parameter instead of `data`.
2460
- Fixed a bug in `domains_handler` where intermediate path segments were sometimes dropped for nested resources like `/credentials` or `/ips`.
2561
- Fixed flaky integration tests failing with `429 Too Many Requests` and `403 Limits Exceeded` by adding proper eventual consistency delays and state teardowns.
2662
- Fixed DKIM key generation tests to use the `-traditional` OpenSSL flag, ensuring valid PKCS1 format compatibility.
2763
- Fixed DKIM selector test names to strictly comply with RFC 6376 formatting (replaced underscores with hyphens).
64+
- Python Data Model Integrity: The Catch-All router (`__getattr__`) now strictly rejects Python magic methods (`__dunder__`), preventing crashes when using `hasattr()`, `pickle`, or `copy.deepcopy()`.
65+
- Version Drift: Corrected endpoints for `spamtraps` and `ip_whitelist` to route to their modern `v2` Mailgun backends.
66+
67+
### Security
68+
69+
- OWASP Credential Protection: Implemented a `SecretAuth` tuple subclass to securely redact the Mailgun API key from accidental exposure in memory dumps, tracebacks, and `repr()` logs.
70+
- OWASP Input Validation: Added strict sanitization in `Client._validate_auth` to strip trailing whitespace and block HTTP Header Injection attacks (rejecting `\n` and `\r` characters in API keys).
71+
- CWE-113 (HTTP Header Injection): Implemented strict CRLF (`\r\n`) sanitization inside `SecurityGuard.sanitize_headers` to block malicious header manipulation.
72+
- Supply Chain Security: Patched a potential OS Command Injection vulnerability in GitHub Actions (`publish.yml`) by safely routing `github.*` contexts through environment variables.
73+
74+
### Pull Requests Merged
75+
76+
- [PR_36](https://github.com/mailgun/mailgun-python/pull/36) - Improve client, update & fix tests
77+
- [PR_35](https://github.com/mailgun/mailgun-python/pull/35) - Removed \_prepare_files logic
78+
- [PR_34](https://github.com/mailgun/mailgun-python/pull/34) - Improve the Config class and routes
79+
- [PR_33](https://github.com/mailgun/mailgun-python/pull/32) - Refactored test framework
80+
- [PR_31](https://github.com/mailgun/mailgun-python/pull/31) - Add missing py.typed in module directory
81+
- [PR_30](https://github.com/mailgun/mailgun-python/pull/30) - build(deps): Bump conda-incubator/setup-miniconda from 3.2.0 to 3.3.0
2882

2983
## [1.6.0] - 2026-01-08
3084

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Feel free to ask anything, and contribute:
1818
- Create a new branch.
1919
- Implement your feature or bug fix.
2020
- Add documentation to it.
21-
- Commit, push, open a pull request and voila.
21+
- Commit, push, open a pull request and voilà.
2222

2323
If you have suggestions on how to improve the guides, please submit an issue in our
2424
[Official API Documentation](https://documentation.mailgun.com).

0 commit comments

Comments
 (0)