Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b536e44
Add the get routes match endpoint
skupriienko Nov 25, 2025
c395fbf
Add the update template version copy endpoint
skupriienko Nov 25, 2025
a97ed10
Add the mailboxes credentials endpoint
skupriienko Nov 27, 2025
873f96e
Add users endpoint
skupriienko Nov 28, 2025
725c5be
Solve merge conflicts
skupriienko Dec 4, 2025
664f606
test: Add test_get_routes_match to RoutesTests and AsyncRoutesTests
skupriienko Dec 4, 2025
021b8b6
test: Add test_update_template_version_copy to TemplatesTests and Asy…
skupriienko Dec 4, 2025
d05a4c7
Remove domain envelopes handler and some users examples
skupriienko Dec 5, 2025
7f3415f
test: Add test_put_mailboxes_credentials to DomainTests and AsyncDoma…
skupriienko Dec 5, 2025
9a3b739
Improve users examples
skupriienko Dec 5, 2025
be8901d
test: Add UsersTests and AsyncUsersTests
skupriienko Dec 5, 2025
7864cb3
Improve handle_templates
skupriienko Dec 9, 2025
7ce50e2
Add get_own_user_details() to users examples
skupriienko Dec 9, 2025
0d653e9
docs: Add credentials and users examples to README
skupriienko Dec 9, 2025
d1bcb1a
Remove redundant print statements
skupriienko Dec 9, 2025
b283832
test: Add test_own_user_details to AsyncUsersTests; mark it xfail bec…
skupriienko Dec 9, 2025
e65b849
test: Add docstrings to users tests
skupriienko Dec 9, 2025
644ec4c
test: Add docstrings to domain credentials tests
skupriienko Dec 9, 2025
8890868
ci: Update pre-commit hooks
skupriienko Dec 9, 2025
b118d89
docs: Update changelog
skupriienko Dec 9, 2025
915128c
docs: Update changelog
skupriienko Dec 9, 2025
f9f9d0a
docs: Update changelog
skupriienko Dec 9, 2025
4d58c46
Fix typo
skupriienko Dec 10, 2025
eae5ffb
Go under Unreleased
skupriienko Dec 10, 2025
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
11 changes: 6 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,21 +104,21 @@ repos:
exclude: ^tests

- repo: https://github.com/PyCQA/pylint
rev: v4.0.3
rev: v4.0.4
hooks:
- id: pylint
args:
- --exit-zero

- repo: https://github.com/asottile/pyupgrade
rev: v3.21.1
rev: v3.21.2
hooks:
- id: pyupgrade
args: [--py310-plus, --keep-runtime-typing]

- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: v0.14.5
rev: v0.14.8
hooks:
# Run the linter.
- id: ruff-check
Expand All @@ -139,12 +139,13 @@ repos:
- id: refurb

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.18.2
rev: v1.19.0
hooks:
- id: mypy
args: [--config-file=./pyproject.toml]
additional_dependencies:
- types-requests
- pytest-order
exclude: ^mailgun/examples/

- repo: https://github.com/RobertCraigie/pyright-python
Expand All @@ -153,7 +154,7 @@ repos:
- id: pyright

- repo: https://github.com/PyCQA/bandit
rev: 1.8.6
rev: 1.9.2
hooks:
- id: bandit
args: ["-c", "pyproject.toml", "-r", "."]
Expand Down
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,47 @@ We [keep a changelog.](http://keepachangelog.com/)

## [Unreleased]

### Added

- Add missing endpoints:

- Add "users", "me" to the `users` key of special cases in the class `Config`.
- Add `handle_users` to `mailgun.handlers.users_handler` for parsing [Users API](https://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/users).
- 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).

- Examples:

- Move credentials examples from `mailgun/examples/domain_examples.py` to `mailgun/examples/credentials_examples.py` and add a new example `put_mailboxes_credentials()`.
- Add the `get_routes_match()` example to `mailgun/examples/routes_examples.py`
- Add the `update_template_version_copy()` example to `mailgun/examples/templates_examples.py`
- Add `mailgun/examples/users_examples.py`

- Docs:

- Add `Credentials` and `Users` sections with examples to `README.md`.
- Add docstrings to the test class `UsersTests` & `AsyncUsersTests` and theirs methods.

- Tests:

- Add `test_put_mailboxes_credentials` to `DomainTests` and `AsyncDomainTests`
- Add `test_get_routes_match` to `RoutesTests` and `AsyncRoutesTests`
- Add `test_update_template_version_copy` to `TemplatesTests ` and `AsyncTemplatesTests `
- Add classes `UsersTests` and `AsyncUsersTests` to `tests/tests.py`.

### Changed

- Update `handle_templates()` in `mailgun/handlers/templates_handler.py` to handle `new_tag`

- Update CI workflows: update `pre-commit` hooks to the latest versions.

- Modify `mypy`'s additional_dependencies in `.pre-commit-config.yaml` to suppress `error: Untyped decorator makes function` by adding `pytest-order`

- Replace spaces with tabs in `Makefile`

### Pull Requests Merged

- [PR_25](https://github.com/mailgun/mailgun-python/pull/25) - Add missing endpoints

## [1.4.0] - 2025-11-20

### Added
Expand Down
22 changes: 11 additions & 11 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export PRINT_HELP_PYSCRIPT

BROWSER := python -c "$$BROWSER_PYSCRIPT"

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

clean-cov:
rm -rf .coverage
Expand All @@ -48,7 +48,7 @@ clean-cov:
rm -rf pytest.xml
rm -rf pytest-coverage.txt

clean-build: ## remove build artifacts
clean-build: ## remove build artifacts
rm -fr build/
rm -fr dist/
rm -fr .eggs/
Expand All @@ -58,19 +58,19 @@ clean-build: ## remove build artifacts
clean-env: ## remove conda environment
conda remove -y -n $(CONDA_ENV_NAME) --all ; conda info

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

clean-test: ## remove test and coverage artifacts
clean-test: ## remove test and coverage artifacts
rm -fr .tox/
rm -f .coverage
rm -fr htmlcov/
rm -fr .pytest_cache

clean-temp: ## remove temp artifacts
clean-temp: ## remove temp artifacts
rm -fr temp/tmp.txt
rm -fr tmp.txt

Expand All @@ -84,21 +84,21 @@ clean-other:
help:
$(PYTHON3) -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)

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

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

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

release: dist ## package and upload a release
release: dist ## package and upload a release
twine upload dist/*

dist: clean ## builds source and wheel package
dist: clean ## builds source and wheel package
python -m build
ls -l dist

Expand All @@ -112,7 +112,7 @@ dev-full: clean ## install the package's development version to a fresh environ
$(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: ## runs pre-commit against files. NOTE: older files are disabled in the pre-commit config.
pre-commit run --all-files

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

coverage: ## check code coverage quickly with the default Python
coverage: ## check code coverage quickly with the default Python
coverage run --source $(SRC_DIR) -m pytest
coverage report -m
coverage html
Expand Down
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ Check out all the resources and Python code examples in the official
- [Create a single validation](#create-a-single-validation)
- [Inbox placement](#inbox-placement)
- [Get all inbox](#get-all-inbox)
- [Credentials](#credentials)
- [List Mailgun SMTP credential metadata for a given domain](#list-mailgun-smtp-credential-metadata-for-a-given-domain)
- [Create Mailgun SMTP credentials for a given domain](#create-mailgun-smtp-credentials-for-a-given-domain)
- [Users](#users)
- [Get users on an account](#get-users-on-an-account)
- [Get a user's details](#)
- [License](#license)
- [Contribute](#contribute)
- [Contributors](#contributors)
Expand Down Expand Up @@ -1307,6 +1313,70 @@ def get_all_inbox() -> None:
print(req.json())
```

### Credentials

#### List Mailgun SMTP credential metadata for a given domain

```python
def get_credentials() -> None:
"""
GET /domains/<domain>/credentials
:return:
"""
request = client.domains_credentials.get(domain=domain)
print(request.json())
```

#### Create Mailgun SMTP credentials for a given domain

```python
def post_credentials() -> None:
"""
POST /domains/<domain>/credentials
:return:
"""
data = {
"login": f"alice_bob@{domain}",
"password": "test_new_creds123", # pragma: allowlist secret
}
request = client.domains_credentials.create(domain=domain, data=data)
print(request.json())
```

### Users

#### Get users on an account

```python
def get_users() -> None:
"""
GET /v5/users
:return:
"""
query = {"role": "admin", "limit": "0", "skip": "0"}
req = client.users.get(filters=query)
print(req.json())
```

#### Get a user's details

```python
def get_user_details() -> None:
"""
GET /v5/users/{user_id}
:return:
"""
mailgun_email = os.environ["MAILGUN_EMAIL"]
query = {"role": "admin", "limit": "0", "skip": "0"}
req1 = client.users.get(filters=query)
users = req1.json()["users"]

for user in users:
if mailgun_email == user["email"]:
req2 = client.users.get(user_id=user["id"])
print(req2.json())
```

## License

[Apache-2.0](https://choosealicense.com/licenses/apache-2.0/)
Expand Down
14 changes: 14 additions & 0 deletions mailgun/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from mailgun.handlers.default_handler import handle_default
from mailgun.handlers.domains_handler import handle_domainlist
from mailgun.handlers.domains_handler import handle_domains
from mailgun.handlers.domains_handler import handle_mailboxes_credentials
from mailgun.handlers.domains_handler import handle_sending_queues
from mailgun.handlers.email_validation_handler import handle_address_validate
from mailgun.handlers.error_handler import ApiError
Expand All @@ -46,6 +47,7 @@
from mailgun.handlers.suppressions_handler import handle_whitelists
from mailgun.handlers.tags_handler import handle_tags
from mailgun.handlers.templates_handler import handle_templates
from mailgun.handlers.users_handler import handle_users


if TYPE_CHECKING:
Expand All @@ -65,6 +67,7 @@
"dkim_selector": handle_domains,
"web_prefix": handle_domains,
"sending_queues": handle_sending_queues,
"mailboxes": handle_mailboxes_credentials,
"ips": handle_ips,
"ip_pools": handle_ippools,
"tags": handle_tags,
Expand All @@ -82,6 +85,7 @@
"events": handle_default,
"analytics": handle_metrics,
"bounce-classification": handle_bounce_classification,
"users": handle_users,
}


Expand Down Expand Up @@ -144,6 +148,10 @@ def __getitem__(self, key: str) -> tuple[Any, dict[str, str]]:
"base": v2_base,
"keys": ["bounce-classification", "metrics"],
},
"users": {
"base": v5_base,
"keys": ["users", "me"],
},
}

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

if "users" in key:
return {
"base": v5_base,
"keys": key.split("_"),
}, headers

# Handle DIPP endpoints
if "subaccount" in key:
if "ip_pools" in key:
Expand Down
75 changes: 75 additions & 0 deletions mailgun/examples/credentials_examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from __future__ import annotations

import os

from mailgun.client import Client


key: str = os.environ["APIKEY"]
domain: str = os.environ["DOMAIN"]

client: Client = Client(auth=("api", key))


def get_credentials() -> None:
"""
GET /domains/<domain>/credentials
:return:
"""
request = client.domains_credentials.get(domain=domain)
print(request.json())


def post_credentials() -> None:
"""
POST /domains/<domain>/credentials
:return:
"""
data = {
"login": f"alice_bob@{domain}",
"password": "test_new_creds123", # pragma: allowlist secret
}
request = client.domains_credentials.create(domain=domain, data=data)
print(request.json())


def put_credentials() -> None:
"""
PUT /domains/<domain>/credentials/<login>
:return:
"""
data = {"password": "test_new_creds12356"} # pragma: allowlist secret
request = client.domains_credentials.put(domain=domain, data=data, login=f"alice_bob@{domain}")
print(request.json())


def put_mailboxes_credentials() -> None:
"""
PUT /v3/{domain_name}/mailboxes/{spec}
:return:
"""

req = client.mailboxes.put(domain=domain, login=f"alice_bob@{domain}")
print(req.json())


def delete_all_domain_credentials() -> None:
"""
DELETE /domains/<domain>/credentials
:return:
"""
request = client.domains_credentials.delete(domain=domain)
print(request.json())


def delete_credentials() -> None:
"""
DELETE /domains/<domain>/credentials/<login>
:return:
"""
request = client.domains_credentials.delete(domain=domain, login=f"alice_bob@{domain}")
print(request.json())


if __name__ == "__main__":
put_mailboxes_credentials()
Loading