Skip to content

Commit 0cae25c

Browse files
authored
Add missing endpoints (#25)
* Add the get routes match endpoint * Add the update template version copy endpoint * Add the mailboxes credentials endpoint * Add users endpoint * test: Add test_get_routes_match to RoutesTests and AsyncRoutesTests * test: Add test_update_template_version_copy to TemplatesTests and AsyncTemplatesTests * Remove domain envelopes handler and some users examples * test: Add test_put_mailboxes_credentials to DomainTests and AsyncDomainTests * Improve users examples * test: Add UsersTests and AsyncUsersTests * Improve handle_templates * Add get_own_user_details() to users examples * docs: Add credentials and users examples to README * test: Add test_own_user_details to AsyncUsersTests; mark it xfail because of a dynamic secret env variable * test: Add docstrings to users tests * test: Add docstrings to domain credentials tests * ci: Update pre-commit hooks * docs: Update changelog
1 parent 2eba81a commit 0cae25c

14 files changed

+899
-99
lines changed

.pre-commit-config.yaml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,21 +104,21 @@ repos:
104104
exclude: ^tests
105105

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

113113
- repo: https://github.com/asottile/pyupgrade
114-
rev: v3.21.1
114+
rev: v3.21.2
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.5
121+
rev: v0.14.8
122122
hooks:
123123
# Run the linter.
124124
- id: ruff-check
@@ -139,12 +139,13 @@ repos:
139139
- id: refurb
140140

141141
- repo: https://github.com/pre-commit/mirrors-mypy
142-
rev: v1.18.2
142+
rev: v1.19.0
143143
hooks:
144144
- id: mypy
145145
args: [--config-file=./pyproject.toml]
146146
additional_dependencies:
147147
- types-requests
148+
- pytest-order
148149
exclude: ^mailgun/examples/
149150

150151
- repo: https://github.com/RobertCraigie/pyright-python
@@ -153,7 +154,7 @@ repos:
153154
- id: pyright
154155

155156
- repo: https://github.com/PyCQA/bandit
156-
rev: 1.8.6
157+
rev: 1.9.2
157158
hooks:
158159
- id: bandit
159160
args: ["-c", "pyproject.toml", "-r", "."]

CHANGELOG.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,47 @@ We [keep a changelog.](http://keepachangelog.com/)
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- Add missing endpoints:
10+
11+
- Add "users", "me" to the `users` key of special cases in the class `Config`.
12+
- Add `handle_users` to `mailgun.handlers.users_handler` for parsing [Users API](https://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/users).
13+
- Add `handle_mailboxes_credentials()` to `mailgun.handlers.domains_handler` for parsing `Update Mailgun SMTP credentials` in [Credentials API](https://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/credentials).
14+
15+
- Examples:
16+
17+
- Move credentials examples from `mailgun/examples/domain_examples.py` to `mailgun/examples/credentials_examples.py` and add a new example `put_mailboxes_credentials()`.
18+
- Add the `get_routes_match()` example to `mailgun/examples/routes_examples.py`
19+
- Add the `update_template_version_copy()` example to `mailgun/examples/templates_examples.py`
20+
- Add `mailgun/examples/users_examples.py`
21+
22+
- Docs:
23+
24+
- Add `Credentials` and `Users` sections with examples to `README.md`.
25+
- Add docstrings to the test class `UsersTests` & `AsyncUsersTests` and theirs methods.
26+
27+
- Tests:
28+
29+
- Add `test_put_mailboxes_credentials` to `DomainTests` and `AsyncDomainTests`
30+
- Add `test_get_routes_match` to `RoutesTests` and `AsyncRoutesTests`
31+
- Add `test_update_template_version_copy` to `TemplatesTests ` and `AsyncTemplatesTests `
32+
- Add classes `UsersTests` and `AsyncUsersTests` to `tests/tests.py`.
33+
34+
### Changed
35+
36+
- Update `handle_templates()` in `mailgun/handlers/templates_handler.py` to handle `new_tag`
37+
38+
- Update CI workflows: update `pre-commit` hooks to the latest versions.
39+
40+
- Modify `mypy`'s additional_dependencies in `.pre-commit-config.yaml` to suppress `error: Untyped decorator makes function` by adding `pytest-order`
41+
42+
- Replace spaces with tabs in `Makefile`
43+
44+
### Pull Requests Merged
45+
46+
- [PR_25](https://github.com/mailgun/mailgun-python/pull/25) - Add missing endpoints
47+
748
## [1.4.0] - 2025-11-20
849

950
### Added

Makefile

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export PRINT_HELP_PYSCRIPT
3939

4040
BROWSER := python -c "$$BROWSER_PYSCRIPT"
4141

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

4444
clean-cov:
4545
rm -rf .coverage
@@ -48,7 +48,7 @@ clean-cov:
4848
rm -rf pytest.xml
4949
rm -rf pytest-coverage.txt
5050

51-
clean-build: ## remove build artifacts
51+
clean-build: ## remove build artifacts
5252
rm -fr build/
5353
rm -fr dist/
5454
rm -fr .eggs/
@@ -58,19 +58,19 @@ clean-build: ## remove build artifacts
5858
clean-env: ## remove conda environment
5959
conda remove -y -n $(CONDA_ENV_NAME) --all ; conda info
6060

61-
clean-pyc: ## remove Python file artifacts
61+
clean-pyc: ## remove Python file artifacts
6262
find . -name '*.pyc' -exec rm -f {} +
6363
find . -name '*.pyo' -exec rm -f {} +
6464
find . -name '*~' -exec rm -f {} +
6565
find . -name '__pycache__' -exec rm -fr {} +
6666

67-
clean-test: ## remove test and coverage artifacts
67+
clean-test: ## remove test and coverage artifacts
6868
rm -fr .tox/
6969
rm -f .coverage
7070
rm -fr htmlcov/
7171
rm -fr .pytest_cache
7272

73-
clean-temp: ## remove temp artifacts
73+
clean-temp: ## remove temp artifacts
7474
rm -fr temp/tmp.txt
7575
rm -fr tmp.txt
7676

@@ -84,21 +84,21 @@ clean-other:
8484
help:
8585
$(PYTHON3) -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
8686

87-
environment: ## handles environment creation
87+
environment: ## handles environment creation
8888
conda env create -f environment.yaml --name $(CONDA_ENV_NAME) --yes
8989
conda run --name $(CONDA_ENV_NAME) pip install .
9090

91-
environment-dev: ## Handles environment creation
91+
environment-dev: ## Handles environment creation
9292
conda env create -n $(CONDA_ENV_NAME)-dev -y --file environment-dev.yml
9393
conda run --name $(CONDA_ENV_NAME)-dev pip install -e .
9494

9595
install: clean ## install the package to the active Python's site-packages
9696
pip install .
9797

98-
release: dist ## package and upload a release
98+
release: dist ## package and upload a release
9999
twine upload dist/*
100100

101-
dist: clean ## builds source and wheel package
101+
dist: clean ## builds source and wheel package
102102
python -m build
103103
ls -l dist
104104

@@ -112,7 +112,7 @@ dev-full: clean ## install the package's development version to a fresh environ
112112
$(CONDA_ACTIVATE) $(CONDA_ENV_NAME)-dev && pre-commit install
113113

114114

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

118118
check-env:
@@ -141,7 +141,7 @@ test-cov: check-env ## checks test coverage requirements
141141
tests-cov-fail:
142142
@pytest --cov=$(SRC_DIR) --cov-report term-missing --cov-report=html --cov-fail-under=80
143143

144-
coverage: ## check code coverage quickly with the default Python
144+
coverage: ## check code coverage quickly with the default Python
145145
coverage run --source $(SRC_DIR) -m pytest
146146
coverage report -m
147147
coverage html

README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ Check out all the resources and Python code examples in the official
9595
- [Create a single validation](#create-a-single-validation)
9696
- [Inbox placement](#inbox-placement)
9797
- [Get all inbox](#get-all-inbox)
98+
- [Credentials](#credentials)
99+
- [List Mailgun SMTP credential metadata for a given domain](#list-mailgun-smtp-credential-metadata-for-a-given-domain)
100+
- [Create Mailgun SMTP credentials for a given domain](#create-mailgun-smtp-credentials-for-a-given-domain)
101+
- [Users](#users)
102+
- [Get users on an account](#get-users-on-an-account)
103+
- [Get a user's details](#)
98104
- [License](#license)
99105
- [Contribute](#contribute)
100106
- [Contributors](#contributors)
@@ -1307,6 +1313,70 @@ def get_all_inbox() -> None:
13071313
print(req.json())
13081314
```
13091315

1316+
### Credentials
1317+
1318+
#### List Mailgun SMTP credential metadata for a given domain
1319+
1320+
```python
1321+
def get_credentials() -> None:
1322+
"""
1323+
GET /domains/<domain>/credentials
1324+
:return:
1325+
"""
1326+
request = client.domains_credentials.get(domain=domain)
1327+
print(request.json())
1328+
```
1329+
1330+
#### Create Mailgun SMTP credentials for a given domain
1331+
1332+
```python
1333+
def post_credentials() -> None:
1334+
"""
1335+
POST /domains/<domain>/credentials
1336+
:return:
1337+
"""
1338+
data = {
1339+
"login": f"alice_bob@{domain}",
1340+
"password": "test_new_creds123", # pragma: allowlist secret
1341+
}
1342+
request = client.domains_credentials.create(domain=domain, data=data)
1343+
print(request.json())
1344+
```
1345+
1346+
### Users
1347+
1348+
#### Get users on an account
1349+
1350+
```python
1351+
def get_users() -> None:
1352+
"""
1353+
GET /v5/users
1354+
:return:
1355+
"""
1356+
query = {"role": "admin", "limit": "0", "skip": "0"}
1357+
req = client.users.get(filters=query)
1358+
print(req.json())
1359+
```
1360+
1361+
#### Get a user's details
1362+
1363+
```python
1364+
def get_user_details() -> None:
1365+
"""
1366+
GET /v5/users/{user_id}
1367+
:return:
1368+
"""
1369+
mailgun_email = os.environ["MAILGUN_EMAIL"]
1370+
query = {"role": "admin", "limit": "0", "skip": "0"}
1371+
req1 = client.users.get(filters=query)
1372+
users = req1.json()["users"]
1373+
1374+
for user in users:
1375+
if mailgun_email == user["email"]:
1376+
req2 = client.users.get(user_id=user["id"])
1377+
print(req2.json())
1378+
```
1379+
13101380
## License
13111381

13121382
[Apache-2.0](https://choosealicense.com/licenses/apache-2.0/)

mailgun/client.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from mailgun.handlers.default_handler import handle_default
3131
from mailgun.handlers.domains_handler import handle_domainlist
3232
from mailgun.handlers.domains_handler import handle_domains
33+
from mailgun.handlers.domains_handler import handle_mailboxes_credentials
3334
from mailgun.handlers.domains_handler import handle_sending_queues
3435
from mailgun.handlers.email_validation_handler import handle_address_validate
3536
from mailgun.handlers.error_handler import ApiError
@@ -46,6 +47,7 @@
4647
from mailgun.handlers.suppressions_handler import handle_whitelists
4748
from mailgun.handlers.tags_handler import handle_tags
4849
from mailgun.handlers.templates_handler import handle_templates
50+
from mailgun.handlers.users_handler import handle_users
4951

5052

5153
if TYPE_CHECKING:
@@ -65,6 +67,7 @@
6567
"dkim_selector": handle_domains,
6668
"web_prefix": handle_domains,
6769
"sending_queues": handle_sending_queues,
70+
"mailboxes": handle_mailboxes_credentials,
6871
"ips": handle_ips,
6972
"ip_pools": handle_ippools,
7073
"tags": handle_tags,
@@ -82,6 +85,7 @@
8285
"events": handle_default,
8386
"analytics": handle_metrics,
8487
"bounce-classification": handle_bounce_classification,
88+
"users": handle_users,
8589
}
8690

8791

@@ -144,6 +148,10 @@ def __getitem__(self, key: str) -> tuple[Any, dict[str, str]]:
144148
"base": v2_base,
145149
"keys": ["bounce-classification", "metrics"],
146150
},
151+
"users": {
152+
"base": v5_base,
153+
"keys": ["users", "me"],
154+
},
147155
}
148156

149157
if key in special_cases:
@@ -165,6 +173,12 @@ def __getitem__(self, key: str) -> tuple[Any, dict[str, str]]:
165173
"keys": f"{part1}-{part2}".split("_"),
166174
}, headers
167175

176+
if "users" in key:
177+
return {
178+
"base": v5_base,
179+
"keys": key.split("_"),
180+
}, headers
181+
168182
# Handle DIPP endpoints
169183
if "subaccount" in key:
170184
if "ip_pools" in key:
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from __future__ import annotations
2+
3+
import os
4+
5+
from mailgun.client import Client
6+
7+
8+
key: str = os.environ["APIKEY"]
9+
domain: str = os.environ["DOMAIN"]
10+
11+
client: Client = Client(auth=("api", key))
12+
13+
14+
def get_credentials() -> None:
15+
"""
16+
GET /domains/<domain>/credentials
17+
:return:
18+
"""
19+
request = client.domains_credentials.get(domain=domain)
20+
print(request.json())
21+
22+
23+
def post_credentials() -> None:
24+
"""
25+
POST /domains/<domain>/credentials
26+
:return:
27+
"""
28+
data = {
29+
"login": f"alice_bob@{domain}",
30+
"password": "test_new_creds123", # pragma: allowlist secret
31+
}
32+
request = client.domains_credentials.create(domain=domain, data=data)
33+
print(request.json())
34+
35+
36+
def put_credentials() -> None:
37+
"""
38+
PUT /domains/<domain>/credentials/<login>
39+
:return:
40+
"""
41+
data = {"password": "test_new_creds12356"} # pragma: allowlist secret
42+
request = client.domains_credentials.put(domain=domain, data=data, login=f"alice_bob@{domain}")
43+
print(request.json())
44+
45+
46+
def put_mailboxes_credentials() -> None:
47+
"""
48+
PUT /v3/{domain_name}/mailboxes/{spec}
49+
:return:
50+
"""
51+
52+
req = client.mailboxes.put(domain=domain, login=f"alice_bob@{domain}")
53+
print(req.json())
54+
55+
56+
def delete_all_domain_credentials() -> None:
57+
"""
58+
DELETE /domains/<domain>/credentials
59+
:return:
60+
"""
61+
request = client.domains_credentials.delete(domain=domain)
62+
print(request.json())
63+
64+
65+
def delete_credentials() -> None:
66+
"""
67+
DELETE /domains/<domain>/credentials/<login>
68+
:return:
69+
"""
70+
request = client.domains_credentials.delete(domain=domain, login=f"alice_bob@{domain}")
71+
print(request.json())
72+
73+
74+
if __name__ == "__main__":
75+
put_mailboxes_credentials()

0 commit comments

Comments
 (0)