Skip to content

Commit 9176406

Browse files
authored
Add support for the Bounce Classification v2 API (#22)
* Add Bounce Classification endpoint * Update pre-commit hooks * Add the Bounce Classification section with an example to README.md * Add class BounceClassificationTests * Apply linters: remove redundant type ignore * Update CHANGELOG
1 parent 4e3f88d commit 9176406

7 files changed

Lines changed: 325 additions & 12 deletions

File tree

.pre-commit-config.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ repos:
7171
exclude: ^tests
7272

7373
- repo: https://github.com/python-jsonschema/check-jsonschema
74-
rev: 0.34.1
74+
rev: 0.35.0
7575
hooks:
7676
- id: check-github-workflows
7777
files: ^\.github/workflows/.*\.ya?ml$
@@ -104,24 +104,24 @@ repos:
104104
exclude: ^tests
105105

106106
- repo: https://github.com/PyCQA/pylint
107-
rev: v4.0.2
107+
rev: v4.0.3
108108
hooks:
109109
- id: pylint
110110
args:
111111
- --exit-zero
112112

113113
- repo: https://github.com/asottile/pyupgrade
114-
rev: v3.21.0
114+
rev: v3.21.1
115115
hooks:
116116
- id: pyupgrade
117117
args: [--py310-plus, --keep-runtime-typing]
118118

119119
- repo: https://github.com/charliermarsh/ruff-pre-commit
120120
# Ruff version.
121-
rev: v0.14.2
121+
rev: v0.14.5
122122
hooks:
123123
# Run the linter.
124-
- id: ruff
124+
- id: ruff-check
125125
args: [--fix, --preview, --exit-non-zero-on-fix]
126126
# Run the formatter.
127127
- id: ruff-format

CHANGELOG.md

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

55
## [Unreleased]
66

7+
## [1.4.0] - 2025-11-XX
8+
9+
### Added
10+
11+
- Add the `Bounce Classification` endpoint:
12+
- Add `bounce-classification`, `metrics` to the `bounceclassification` key of special cases in the class `Config`.
13+
- Add `bounce_classification_handler.py` to parse Bounce Classification API.
14+
- Add `mailgun/examples/bounce_classification_examples.py` with `post_list_statistic_v2()`.
15+
- Add `Bounce Classification` sections with an example to `README.md`.
16+
- Add class `BounceClassificationTests ` to `tests/tests.py`.
17+
- Add docstrings to the test class `BounceClassificationTests` and its methods.
18+
19+
### Changed
20+
21+
- Fix `Metrics`, `Tags New` & `Logs` docstrings in tests.
22+
- Update CI workflows: update `pre-commit` hooks to the latest versions.
23+
- Apply linters: remove redundant `type: ignore`.
24+
25+
### Pull Requests Merged
26+
27+
- [PR_22](https://github.com/mailgun/mailgun-python/pull/22) - Add support for the Bounce Classification v2 API
28+
729
## [1.3.0] - 2025-11-08
830

931
### Added
@@ -143,4 +165,6 @@ We [keep a changelog.](http://keepachangelog.com/)
143165
[1.0.1]: https://github.com/mailgun/mailgun-python/releases/tag/v1.0.1
144166
[1.0.2]: https://github.com/mailgun/mailgun-python/releases/tag/v1.0.2
145167
[1.1.0]: https://github.com/mailgun/mailgun-python/releases/tag/v1.1.0
146-
[unreleased]: https://github.com/mailgun/mailgun-python/compare/v1.1.0...HEAD
168+
[1.2.0]: https://github.com/mailgun/mailgun-python/releases/tag/v1.2.0
169+
[1.3.0]: https://github.com/mailgun/mailgun-python/releases/tag/v1.3.0
170+
[unreleased]: https://github.com/mailgun/mailgun-python/compare/v1.3.0...HEAD

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ Check out all the resources and Python code examples in the official
4747
- [Events](#events)
4848
- [Retrieves a paginated list of events](#retrieves-a-paginated-list-of-events)
4949
- [Get events by recipient](#get-events-by-recipient)
50+
- [Bounce Classification](#bounce-classification)
51+
- [List statistic v2](#list-statistic-v2)
5052
- [Logs](#logs)
5153
- [List logs](#list-logs)
5254
- [Tags New](#tags-new)
@@ -573,6 +575,59 @@ def events_by_recipient() -> None:
573575
print(req.json())
574576
```
575577

578+
### Bounce Classification
579+
580+
[API endpoint](https://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/bounce-classification).
581+
582+
#### List statistic v2
583+
584+
Items that have no bounces and no delays(classified_failures_count==0) are not returned.
585+
586+
```python
587+
def post_list_statistic_v2() -> None:
588+
"""
589+
# Bounce Classification
590+
# POST /v2/bounce-classification/metrics
591+
:return:
592+
"""
593+
594+
payload = {
595+
"start": "Wed, 12 Nov 2025 23:00:00 UTC",
596+
"end": "Thu, 13 Nov 2025 23:00:00 UTC",
597+
"resolution": "day",
598+
"duration": "24h0m0s",
599+
"dimensions": ["entity-name", "domain.name"],
600+
"metrics": [
601+
"critical_bounce_count",
602+
"non_critical_bounce_count",
603+
"critical_delay_count",
604+
"non_critical_delay_count",
605+
"delivered_smtp_count",
606+
"classified_failures_count",
607+
"critical_bounce_rate",
608+
"non_critical_bounce_rate",
609+
"critical_delay_rate",
610+
"non_critical_delay_rate",
611+
],
612+
"filter": {
613+
"AND": [
614+
{
615+
"attribute": "domain.name",
616+
"comparator": "=",
617+
"values": [{"value": domain}],
618+
}
619+
]
620+
},
621+
"include_subaccounts": True,
622+
"pagination": {"sort": "entity-name:asc", "limit": 10},
623+
}
624+
625+
headers = {"Content-Type": "application/json"}
626+
627+
req = client.bounceclassification_metrics.create(data=payload, headers=headers)
628+
print(req.json())
629+
```
630+
576631
### Logs
577632

578633
Mailgun keeps track of every inbound and outbound message event and stores this log data. This data can be queried and

mailgun/client.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import requests
2121

22+
from mailgun.handlers.bounce_classification_handler import handle_bounce_classification
2223
from mailgun.handlers.default_handler import handle_default
2324
from mailgun.handlers.domains_handler import handle_domainlist
2425
from mailgun.handlers.domains_handler import handle_domains
@@ -71,6 +72,7 @@
7172
"messages.mime": handle_default,
7273
"events": handle_default,
7374
"analytics": handle_metrics,
75+
"bounce-classification": handle_bounce_classification,
7476
}
7577

7678

@@ -107,6 +109,7 @@ def __getitem__(self, key: str) -> tuple[Any, dict[str, str]]:
107109
key = key.lower()
108110
headers = {"User-agent": self.user_agent}
109111
v1_base = urljoin(self.api_url, "v1/")
112+
v2_base = urljoin(self.api_url, "v2/")
110113
v3_base = urljoin(self.api_url, "v3/")
111114
v4_base = urljoin(self.api_url, "v4/")
112115
v5_base = urljoin(self.api_url, "v5/")
@@ -127,6 +130,11 @@ def __getitem__(self, key: str) -> tuple[Any, dict[str, str]]:
127130
"base": v1_base,
128131
"keys": ["analytics", "usage", "metrics", "logs", "tags", "limits"],
129132
},
133+
# /v2/bounce-classification/metrics
134+
"bounceclassification": {
135+
"base": v2_base,
136+
"keys": ["bounce-classification", "metrics"],
137+
},
130138
}
131139

132140
if key in special_cases:
@@ -139,6 +147,15 @@ def __getitem__(self, key: str) -> tuple[Any, dict[str, str]]:
139147
"keys": key.split("_"),
140148
}, headers
141149

150+
if "bounceclassification" in key:
151+
headers |= {"Content-Type": "application/json"}
152+
part1 = key[:6]
153+
part2 = key[6:]
154+
return {
155+
"base": v2_base,
156+
"keys": f"{part1}-{part2}".split("_"),
157+
}, headers
158+
142159
# Handle DIPP endpoints
143160
if "subaccount" in key:
144161
if "ip_pools" in key:
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import os
2+
3+
from mailgun.client import Client
4+
5+
6+
key: str = os.environ["APIKEY"]
7+
domain: str = os.environ["DOMAIN"]
8+
client: Client = Client(auth=("api", key))
9+
10+
11+
def post_list_statistic_v2() -> None:
12+
"""
13+
# Bounce Classification
14+
# POST /v2/bounce-classification/metrics
15+
:return:
16+
"""
17+
18+
payload = {
19+
"start": "Wed, 12 Nov 2025 23:00:00 UTC",
20+
"end": "Thu, 13 Nov 2025 23:00:00 UTC",
21+
"resolution": "day",
22+
"duration": "24h0m0s",
23+
"dimensions": ["entity-name", "domain.name"],
24+
"metrics": [
25+
"critical_bounce_count",
26+
"non_critical_bounce_count",
27+
"critical_delay_count",
28+
"non_critical_delay_count",
29+
"delivered_smtp_count",
30+
"classified_failures_count",
31+
"critical_bounce_rate",
32+
"non_critical_bounce_rate",
33+
"critical_delay_rate",
34+
"non_critical_delay_rate",
35+
],
36+
"filter": {
37+
"AND": [
38+
{
39+
"attribute": "domain.name",
40+
"comparator": "=",
41+
"values": [{"value": domain}],
42+
}
43+
]
44+
},
45+
"include_subaccounts": True,
46+
"pagination": {"sort": "entity-name:asc", "limit": 10},
47+
}
48+
49+
headers = {"Content-Type": "application/json"}
50+
51+
req = client.bounceclassification_metrics.create(data=payload, headers=headers)
52+
print(req.json())
53+
54+
55+
if __name__ == "__main__":
56+
post_list_statistic_v2()
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""BOUNCE CLASSIFICATION HANDLER.
2+
3+
Doc: https://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/bounce-classification
4+
"""
5+
6+
from __future__ import annotations
7+
8+
from os import path
9+
from typing import Any
10+
11+
12+
def handle_bounce_classification(
13+
url: dict[str, Any],
14+
_domain: str | None,
15+
_method: str | None,
16+
**kwargs: Any,
17+
) -> Any:
18+
"""Handle Bounce Classification.
19+
20+
:param url: Incoming URL dictionary
21+
:type url: dict
22+
:param _domain: Incoming domain (it's not being used for this handler)
23+
:type _domain: str
24+
:param _method: Incoming request method (it's not being used for this handler)
25+
:type _method: str
26+
:param kwargs: kwargs
27+
:return: final url for Bounce Classification endpoints
28+
"""
29+
final_keys = path.join("/", *url["keys"]) if url["keys"] else ""
30+
31+
return url["base"][:-1] + final_keys

0 commit comments

Comments
 (0)