Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
96383aa
refactor: Improve the Config class; use data-driven routing approach
skupriienko Mar 13, 2026
cc39522
refactor: Improve Config.__getitem__; use routes.py
skupriienko Mar 24, 2026
4fadb10
Fix bugs in tests
skupriienko Mar 24, 2026
d45660c
ci: Update pre-commit hooks
skupriienko Mar 24, 2026
3c59723
Fix bugs in tests
skupriienko Mar 24, 2026
8768279
ci: Remove format pyproject hook
skupriienko Mar 24, 2026
65fca63
Solve merge conflicts
skupriienko Mar 25, 2026
eed000e
Solve merge conflicts
skupriienko Mar 25, 2026
59874dc
Remove _format_exact; improve handle_domainlist
skupriienko Mar 25, 2026
217bade
Move logic related to json headers to __getitem__
skupriienko Mar 25, 2026
840219b
Fix Content-Type
skupriienko Mar 25, 2026
cbcc7a4
Fix Content-Type
skupriienko Mar 25, 2026
4072a7a
Fix Endpoint.create and Endpoint.update, protect original headers, wh…
skupriienko Mar 25, 2026
f92bf49
refactor(client): simplify endpoint initialization in __getattr__
skupriienko Mar 25, 2026
3319a8c
refactor(client): Fix raising exceptions
skupriienko Mar 25, 2026
a953bc4
Improve tests and domain example
skupriienko Mar 27, 2026
251a59d
ci: Improve dependabot
skupriienko Mar 27, 2026
7115a8c
ci: Improve CI workflows
skupriienko Mar 27, 2026
bb132bd
Fix tests
skupriienko Mar 28, 2026
632aa9a
Improve messages example
skupriienko Mar 28, 2026
cafe05e
Improve examples with csv files
skupriienko Mar 28, 2026
fd4c6d4
Update recipe
skupriienko Mar 28, 2026
2f0eb6e
Fix final_keys in handlers
skupriienko Mar 28, 2026
79181f7
refact(handlers): Improve and fix handlers and type hints
skupriienko Mar 28, 2026
bbff89b
Add logging, fix endpoints, handlers, tests
skupriienko Mar 30, 2026
6d3080b
Fix async tests
skupriienko Mar 30, 2026
3f446d6
Add build_path_from_keys to handlers
skupriienko Mar 30, 2026
9627772
docs: Update changelog, add logging example to readme
skupriienko Mar 30, 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
3 changes: 3 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ updates:
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
groups:
minor-and-patch:
update-types: [ "minor", "patch" ]
python-packages:
patterns:
- "*"
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/commit_checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,11 @@ jobs:

- name: Test package imports
run: python -c "import mailgun"

- name: Install test dependencies
run: |
python -m pip install --upgrade pip
pip install pytest

- name: Tests
run: pytest -v tests/unit/
5 changes: 5 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,18 @@ jobs:
run: |
# Force clean version
export SETUPTOOLS_SCM_PRETEND_VERSION=$VERSION
pip install build
python -m build

- name: Check dist
run: |
ls -alh
twine check dist/*

- name: Verify wheel contents
run: |
unzip -l dist/*.whl

# Always publish to TestPyPI for all tags and releases
- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
Expand Down
53 changes: 27 additions & 26 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ repos:
name: "🌳 git · Validate commit format"

- repo: https://github.com/commitizen-tools/commitizen
rev: v4.11.1
rev: v4.13.9
hooks:
- id: commitizen
name: "🌳 git · Validate commit message"
Expand All @@ -123,13 +123,13 @@ repos:
name: "🔒 security · Detect committed secrets"

- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.0
rev: v8.30.1
hooks:
- id: gitleaks
name: "🔒 security · Scan for hardcoded secrets"

- repo: https://github.com/PyCQA/bandit
rev: 1.9.2
rev: 1.9.4
hooks:
- id: bandit
name: "🔒 security · Check Python vulnerabilities"
Expand All @@ -155,14 +155,14 @@ repos:

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

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

# Python code formatting (order matters: autoflake → pyupgrade → darker/ruff)
- repo: https://github.com/PyCQA/autoflake
rev: v2.3.1
rev: v2.3.3
hooks:
- id: autoflake
name: "🐍 format · Remove unused imports"
Expand All @@ -196,14 +196,14 @@ repos:
name: "🐍 format · Format changed lines"
additional_dependencies: [black]

- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.14.10
hooks:
- id: ruff-check
name: "🐍 lint · Check with Ruff"
args: [--fix, --preview, --exit-non-zero-on-fix]
- id: ruff-format
name: "🐍 format · Format with Ruff"
# - 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
Expand All @@ -223,15 +223,15 @@ repos:
exclude: ^tests

- repo: https://github.com/PyCQA/pylint
rev: v4.0.4
rev: v4.0.5
hooks:
- id: pylint
name: "🐍 lint · Check code quality"
args:
- --exit-zero

- repo: https://github.com/dosisod/refurb
rev: v2.2.0
rev: v2.3.0
hooks:
- id: refurb
name: "🐍 performance · Suggest modernizations"
Expand All @@ -252,8 +252,9 @@ repos:
hooks:
- id: interrogate
name: "📝 docs · Check docstring coverage"
exclude: ^(tests)
args: [ --verbose, --fail-under=53, --ignore-init-method ]
exclude: ^(tests|.*/examples)$
pass_filenames: false
args: [ --verbose, --fail-under=43, --ignore-init-method ]

# Python type checking
- repo: https://github.com/pre-commit/mirrors-mypy
Expand All @@ -263,19 +264,19 @@ repos:
name: "🐍 types · Check with mypy"
args: [--config-file=./pyproject.toml]
additional_dependencies:
- types-requests
- pytest-order
- types-requests
exclude: ^mailgun/examples/

- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.407
rev: v1.1.408
hooks:
- id: pyright
name: "🐍 types · Check with pyright"

# Python project configuration
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.24.1
rev: v0.25
hooks:
- id: validate-pyproject
name: "🐍 config · Validate pyproject.toml"
Expand Down Expand Up @@ -314,8 +315,8 @@ repos:
# name: "📝 docs · Check markdown links"

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

## [Unreleased]

### Added

- 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`.
- Added `build_path_from_keys` utility in `mailgun.handlers.utils` to centralize and dry up URL path generation across handlers.

### 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.
- Modernized the codebase using modern Python idioms (e.g., `contextlib.suppress`) and resolved strict typing errors for `pyright`.
- Updated Dependabot configuration to group minor and patch updates and limit open PRs.

### Fixed

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

## [1.6.0] - 2026-01-08

### Added
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ check-env:

test: test-unit


test-unit: ## run unit tests only (no API key required)
$(PYTHON3) -m pytest -v --capture=no $(TEST_DIR)/unit/

Expand Down
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,29 @@ response. In the unlikely case you encounter them and need them raised, please r
**500** - Internal Error on the Mailgun side. Retries are recommended with exponential or logarithmic retry intervals.
If the issue persists, please reach out to our support team.

### Logging & Debugging

The Mailgun SDK includes built-in logging to help you troubleshoot API requests, inspect generated URLs, and read server error messages (like `400 Bad Request` or `404 Not Found`).

The SDK uses the standard Python `logging` module under the namespace `mailgun.client`.

To enable detailed logging in your application, configure the logger before initializing the client:

```python
import logging
from mailgun.client import Client

# Enable DEBUG level for the Mailgun SDK logger
logging.getLogger("mailgun.client").setLevel(logging.DEBUG)

# Configure the basic console output (if not already configured in your app)
logging.basicConfig(format="%(levelname)s - %(name)s - %(message)s")

# Now, any API errors or requests will be printed to your console
client = Client(auth=("api", "YOUR_API_KEY"))
client.domains.get()
```

## Request examples

### Full list of supported endpoints
Expand Down Expand Up @@ -545,18 +568,28 @@ def post_dkim_keys() -> None:
POST /v1/dkim/keys
:return:
"""
import os
import re
import subprocess
from pathlib import Path

secret_key_filename: str = os.environ["SECRET_KEY_FILENAME"]
secret_key_path: Path = Path(secret_key_filename)
ALLOWED_FILENAME_RE = re.compile(r"^[a-zA-Z0-9._-]{1,255}$")

# Private key PEM file must be generated in PKCS1 format. You need 'openssl' on your machine
# example:
# openssl genrsa -traditional -out .server.key 2048
subprocess.run(["openssl", "genrsa", "-traditional", "-out", ".server.key", "2048"])
if not ALLOWED_FILENAME_RE.match(secret_key_filename):
raise ValueError(f"Invalid filename: {secret_key_filename!r}")
subprocess.run(
["openssl", "genrsa", "-traditional", "-out", secret_key_filename, "--", "2048"], check=True
)

files = [
(
"pem",
("server.key", Path(".server.key").read_bytes()),
("server.key", secret_key_path.read_bytes()),
)
]

Expand Down
13 changes: 6 additions & 7 deletions conda.recipe/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,24 @@ requirements:
{% endfor %}
run:
- python
{% for dep in pyproject['project']['dependencies'] %}
- {{ dep.lower() }}
{% endfor %}
- httpx >=0.24
- requests >=2.32.5
- typing-extensions >=4.7.1 # [py<311]

test:
imports:
- mailgun
- mailgun.handlers
- mailgun.examples
source_files:
- tests/tests.py
- tests/unit/
requires:
- pip
- pytest
- pytest-asyncio
commands:
- pip check
# Important: export required environment variables for integration tests.
# Skip test_update_simple_domain because it can fail.
- pytest tests/tests.py -v -k "not test_update_simple_domain"
- pytest tests/unit/ -v

about:
home: {{ project['urls']['Homepage'] }}
Expand Down
2 changes: 1 addition & 1 deletion mailgun/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.6.0"
__version__ = "1.6.0.post1.dev77"
Loading
Loading