Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c447874
refactor: Improve client and update & fix tests
skupriienko Apr 3, 2026
6c56809
Fix refurb pre-commit hook
skupriienko Apr 3, 2026
280fcf0
ci: Update CI workflows, enable py314, enable ruff pre-commit hook
skupriienko Apr 3, 2026
b6037e4
ci: Update CI workflows, enable py314, enable ruff pre-commit hook
skupriienko Apr 3, 2026
1eaaffd
refactor: Improve the Config class; use data-driven routing approach
skupriienko Apr 3, 2026
ffc148d
style: Fix Makefile linting
skupriienko Apr 3, 2026
679764d
style: Apply ruff linter and remove items from ignore list
skupriienko Apr 3, 2026
a11d53c
style: Use absolute imports
skupriienko Apr 3, 2026
a0d19b8
refactor: Speedup and security improvments
skupriienko Apr 4, 2026
7afb791
docs: Use Google style docstrings
skupriienko Apr 5, 2026
60a1c85
docs: Use Google style docstrings in handlers
skupriienko Apr 5, 2026
5755fbd
test: Extend expected_items_keys
skupriienko Apr 5, 2026
d0882b4
docs: Update changelog
skupriienko Apr 5, 2026
2634879
docs: Update readme
skupriienko Apr 5, 2026
27794c6
feat: Define the root public API of the Mailgun SDK
skupriienko Apr 5, 2026
d11ce1d
refactor: stabilize dynamic routing engine and introduce live meta-tests
skupriienko Apr 6, 2026
310e0f7
docs: update README with Validations/Optimize API examples and fix TOC
skupriienko Apr 6, 2026
07dbb3e
docs: update changelog with dynamic routing expansion and path interp…
skupriienko Apr 6, 2026
3c581d9
feat: implement API deprecation warnings and sync integration tests
skupriienko Apr 7, 2026
d8d0ef9
docs: Update changelog; restructure README with Quick Start, Usage, a…
skupriienko Apr 7, 2026
bedcff4
Clean up
skupriienko Apr 7, 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
13 changes: 7 additions & 6 deletions .github/workflows/commit_checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ name: CI

on:
push:
branches:
- main
branches: [main]
pull_request:
branches: [main]

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

- name: Install the package
- name: Install package
run: |
python -m pip install --upgrade pip
pip install .
conda info

Expand All @@ -60,5 +61,5 @@ jobs:
python -m pip install --upgrade pip
pip install pytest

- name: Tests
- name: Run Unit Tests
run: pytest -v tests/unit/
6 changes: 4 additions & 2 deletions .github/workflows/pr_validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ jobs:

- name: Build package
run: |
pip install --upgrade build setuptools wheel setuptools-scm
pip install --upgrade build setuptools wheel setuptools-scm twine
python -m build
twine check dist/*

- name: Test installation
run: |
# Install the built wheel to ensure packaging didn't miss files
pip install dist/*.whl
python -c "from importlib.metadata import version; print(version('mailgun'))"
python -c "import mailgun; from importlib.metadata import version; print(f'Successfully installed v{version(\"mailgun\")}')"
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ permissions:

jobs:
publish:
name: Build and Publish to PyPI
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
95 changes: 18 additions & 77 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -170,57 +170,14 @@ repos:
name: "🔧 ci/cd · Validate GitHub workflows"
files: ^\.github/workflows/.*\.ya?ml$

# Python code formatting (order matters: autoflake → pyupgrade → darker/ruff)
- repo: https://github.com/PyCQA/autoflake
rev: v2.3.3
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.9
hooks:
- id: autoflake
name: "🐍 format · Remove unused imports"
args:
- --in-place
- --remove-all-unused-imports
- --remove-unused-variable
- --ignore-init-module-imports

- repo: https://github.com/asottile/pyupgrade
rev: v3.21.2
hooks:
- id: pyupgrade
name: "🐍 format · Modernize syntax"
args: [--py310-plus, --keep-runtime-typing]

- repo: https://github.com/akaihola/darker
rev: v3.0.0
hooks:
- id: darker
name: "🐍 format · Format changed lines"
additional_dependencies: [black]

# - repo: https://github.com/astral-sh/ruff-pre-commit
# rev: v0.15.6
# hooks:
# - id: ruff-check
# name: "🐍 lint · Check with Ruff"
# args: [--fix, --preview]
# - id: ruff-format
# name: "🐍 format · Format with Ruff"

# Python linting (comprehensive checks)
- repo: https://github.com/pycqa/flake8
rev: 7.3.0
hooks:
- id: flake8
name: "🐍 lint · Check style (Flake8)"
args: ["--ignore=E501,C901", --max-complexity=13] # Sets McCabe complexity limit
additional_dependencies:
- radon
- flake8-docstrings
- Flake8-pyproject
- flake8-bugbear
- flake8-comprehensions
- flake8-tidy-imports
- pycodestyle
exclude: ^tests
- id: ruff-check
name: "🐍 lint · Check with Ruff"
args: [--fix, --preview]
- id: ruff-format
name: "🐍 format · Format with Ruff"

- repo: https://github.com/PyCQA/pylint
rev: v4.0.5
Expand All @@ -230,23 +187,6 @@ repos:
args:
- --exit-zero

- repo: https://github.com/dosisod/refurb
rev: v2.3.0
hooks:
- id: refurb
name: "🐍 performance · Suggest modernizations"
# TODO: Fix FURB147.
args: ["--enable-all", "--ignore", "FURB147"]

# Python documentation
- repo: https://github.com/pycqa/pydocstyle
rev: 6.3.0
hooks:
- id: pydocstyle
name: "🐍 docs · Validate docstrings"
args: [--select=D200,D213,D400,D415]
additional_dependencies: [tomli]

- repo: https://github.com/econchick/interrogate
rev: 1.7.0
hooks:
Expand Down Expand Up @@ -307,16 +247,17 @@ repos:
- mdformat-gfm
- mdformat-black
- mdformat-ruff

# TODO: Enable it for a single check
# - repo: https://github.com/tcort/markdown-link-check
# rev: v3.14.2
# hooks:
# - id: markdown-link-check
# name: "📝 docs · Check markdown links"
- repo: https://github.com/tcort/markdown-link-check
rev: v3.14.2
hooks:
- id: markdown-link-check
name: "📝 docs · Check markdown links"

# Makefile linting
# - repo: https://github.com/checkmake/checkmake
# rev: v0.3.0
# hooks:
# - id: checkmake
# name: "🔧 build · Lint Makefile"
- repo: https://github.com/checkmake/checkmake
rev: v0.3.0
hooks:
- id: checkmake
name: "🔧 build · Lint Makefile"
43 changes: 42 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,67 @@ We [keep a changelog.](http://keepachangelog.com/)

### Added

- Explicit `__all__` declaration in `mailgun.client` to cleanly isolate the public API namespace.
- A `__repr__` method to the `Client` and `BaseEndpoint` classes to improve developer experience (DX) during console debugging (showing target routes instead of memory addresses).
- Security guardrail (CWE-319) in `Config` that logs a warning if a cleartext `http://` API URL is configured.
- Python 3.14 support to the GitHub Actions test matrix.
- Implemented Smart Logging (telemetry) in `Client` and `AsyncClient` to help users debug API requests, generated URLs, and server errors (`404`, `400`, `429`).
- Added a new "Logging & Debugging" section to `README.md`.
- 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`.
- 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).
- Added `build_path_from_keys` utility in `mailgun.handlers.utils` to centralize and dry up URL path generation across handlers.
- Overrode __dir__ in Client and AsyncClient to expose dynamic endpoint routes (e.g., .messages, .domains) directly to IDE autocompletion engines (VS Code, PyCharm).
- Native dynamic routing support for Mailgun Optimize, Validations Service, and Email Preview APIs without requiring new custom handlers.
- Advanced path interpolation in `handle_default` to automatically inject inline URL parameters (e.g., `/v2/x509/{domain}/status`).
- Added a new "Logging & Debugging" section to `README.md`.
- An intelligent live meta-testing suite (`test_routing_meta_live.py`) to strictly verify SDK endpoint aliases against live Mailgun servers.

### Changed

- Refactored the `Config` routing engine to use a deterministic, data-driven approach (`EXACT_ROUTES` and `PREFIX_ROUTES`) for better maintainability.
- 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`.
- Secured internal configuration registries by wrapping them in `MappingProxyType` to prevent accidental mutations of the client state.
- 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.
- **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.
- **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.
- Updated `DOMAIN_ENDPOINTS` mapping to reflect Mailgun's latest architecture, officially moving `tracking`, `click`, `open`, `unsubscribe`, and `webhooks` from `v1` to `v3`.
- Modernized the codebase using modern Python idioms (e.g., `contextlib.suppress`) and resolved strict typing errors for `pyright`.
- **Documentation**: Migrated all internal and public docstrings from legacy Sphinx/reST format to modern Google Style for cleaner readability and better IDE hover-hints.
- Updated Dependabot configuration to group minor and patch updates and limit open PRs.
- Migrated the fragmented linting and formatting pipeline (Flake8, Black, Pylint, Pyupgrade, etc.) to a unified, high-performance `ruff` setup in `.pre-commit-config.yaml`.
- Refactored `api_call` exception blocks to use the `else` clause for successful returns, adhering to strict Ruff (TRY300) standards.
- Enabled pip dependency caching in GitHub Actions to drastically speed up CI workflows.
- Fixed API versioning collisions in `DOMAIN_ENDPOINTS` (e.g., ensuring `tracking` correctly resolves to `v3` instead of `v1`).
- Corrected the `credentials` route prefix to properly inject the `domains/` path segment.
- Updated `README.md` with new documentation, IDE DX features, and code examples for Validations & Optimize APIs.
- Cleaned up obsolete unit tests that conflicted with the new forgiving dynamic Catch-All routing architecture.

### Fixed

- Fixed a silent data loss bug in `create()` where custom `headers` passed by the user were ignored instead of being merged into the request.
- Fixed a kwargs collision bug in `update()` by using `.pop("headers")` instead of `.get()` to prevent passing duplicate keyword arguments to the underlying request.
- Preserved original tracebacks (PEP 3134) by properly chaining `TimeoutError` and `ApiError` using `from e`.
- Used safely truncating massive HTML error responses to 500 characters (preventing a log-flooding vulnerability (OWASP CWE-532)).
- Replaced a fragile `try/except TypeError` status code check with robust `getattr` and `isinstance` validation to prevent masking unrelated exceptions.
- Resolved `httpx` `DeprecationWarning` in `AsyncEndpoint` by properly routing serialized JSON string payloads to the `content` parameter instead of `data`.
- Fixed a bug in `domains_handler` where intermediate path segments were sometimes dropped for nested resources like `/credentials` or `/ips`.
- Fixed flaky integration tests failing with `429 Too Many Requests` and `403 Limits Exceeded` by adding proper eventual consistency delays and state teardowns.
- Fixed DKIM key generation tests to use the `-traditional` OpenSSL flag, ensuring valid PKCS1 format compatibility.
- Fixed DKIM selector test names to strictly comply with RFC 6376 formatting (replaced underscores with hyphens).

### Security

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

### Pull Requests Merged

- [PR_36](https://github.com/mailgun/mailgun-python/pull/36) - Improve client, update & fix tests
- [PR_35](https://github.com/mailgun/mailgun-python/pull/35) - Removed \_prepare_files logic
- [PR_34](https://github.com/mailgun/mailgun-python/pull/34) - Improve the Config class and routes
- [PR_33](https://github.com/mailgun/mailgun-python/pull/32) - Refactored test framework
- [PR_31](https://github.com/mailgun/mailgun-python/pull/31) - Add missing py.typed in module directory
- [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

## [1.6.0] - 2026-01-08

### Added
Expand Down
25 changes: 8 additions & 17 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export PRINT_HELP_PYSCRIPT

BROWSER := python -c "$$BROWSER_PYSCRIPT"

# Point 'all' to whatever you want to happen when someone just types `make`
all: help

clean: clean-cov clean-build clean-pyc clean-test clean-temp clean-other ## remove all build, test, coverage and Python artifacts

clean-cov:
Expand Down Expand Up @@ -111,22 +114,13 @@ dev-full: clean ## install the package's development version to a fresh environ
conda run --name $(CONDA_ENV_NAME)-dev pip install -e .
$(CONDA_ACTIVATE) $(CONDA_ENV_NAME)-dev && pre-commit install


pre-commit: ## runs pre-commit against files. NOTE: older files are disabled in the pre-commit config.
pre-commit run --all-files

check-env:
@missing=0; \
for v in $(REQUIRED_VARS); do \
if [ -z "$${!v}" ]; then \
echo "Missing required env var: $$v"; \
missing=1; \
fi; \
done; \
if [ $$missing -ne 0 ]; then \
echo "Aborting tests due to missing env vars."; \
exit 1; \
fi
@if [ -z "$(ENV_VAR)" ]; then echo "Missing ENV_VAR"; exit 1; fi; \
if [ -z "$(OTHER_VAR)" ]; then echo "Missing OTHER_VAR"; exit 1; fi; \
echo "Environment checks passed."

test: test-unit

Expand Down Expand Up @@ -169,11 +163,8 @@ format-black:
@black --line-length=100 $(SRC_DIR) $(TEST_DIR) $(SCRIPTS_DIR)
format-isort:
@isort --profile black --line-length=88 $(SRC_DIR) $(TEST_DIR) $(SCRIPTS_DIR)
format: format-black format-isort

format: ## runs the code auto-formatter
isort
black
format:
ruff check --fix .

format-docs: ## runs the docstring auto-formatter. Note this requires manually installing `docconvert` with `pip`
docconvert --in-place --config .docconvert.json $(SRC_DIR)
Expand Down
Loading
Loading