diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b101ec0..a966f0b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,3 +9,11 @@ updates: python-packages: patterns: - "*" + + # Enable version updates for GitHub Actions + - package-ecosystem: 'github-actions' + # Workflow files stored in the default location of `.github/workflows` + # You don't need to specify `/.github/workflows` for `directory`. You can use `directory: "/"`. + directory: '/' + schedule: + interval: 'weekly' diff --git a/.gitignore b/.gitignore index 5895d7e..40ce45b 100644 --- a/.gitignore +++ b/.gitignore @@ -302,3 +302,6 @@ dev/ pytestdebug.log */_version.py + +# local temp files +.server.key diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e221f0c..c3df5f8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,9 +3,27 @@ # pre-commit run --all-files # Update this file: # pre-commit autoupdate + +exclude: | + (?x)^( + .*\{\{.*\}\}.*| # Exclude any files with cookiecutter variables + docs/site/.*| # Exclude mkdocs compiled files + \.history/.*| # Exclude history files + .*cache.*/.*| # Exclude cache directories + .*venv.*/.*| # Exclude virtual environment directories + .*/versioneer\.py| + .*/_version\.py| + .*/.*\.svg + )$ + +fail_fast: true + +default_install_hook_types: + - pre-commit + - commit-msg + default_language_version: python: python3 -exclude: ^(.*/versioneer\.py|.*/_version\.py|.*/.*\.svg) ci: autofix_commit_msg: | @@ -19,129 +37,237 @@ ci: skip: [] submodules: false +# .pre-commit-config.yaml repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: + # Python-specific checks - id: check-ast + name: "🐍 python · Validate syntax" - id: check-builtin-literals - - id: fix-byte-order-marker - - id: check-case-conflict + name: "🐍 python · Use literal syntax" - id: check-docstring-first - - id: check-vcs-permalinks - # Fail if staged files are above a certain size. - # To add a large file, use 'git lfs track ; git add to track large files with - # git-lfs rather than committing them directly to the git history + name: "🐍 python · Validate docstring placement" + - id: debug-statements + name: "🐍 python · Detect debug statements" + language_version: python3 + + # Git workflow protection - id: check-added-large-files - args: [ "--maxkb=500" ] - # Fails if there are any ">>>>>" lines in files due to merge conflicts. + name: "🌳 git · Block large files" + args: ['--maxkb=500'] - id: check-merge-conflict - # ensure syntaxes are valid + name: "🌳 git · Detect conflict markers" + - id: forbid-new-submodules + name: "🌳 git · Prevent submodules" + - id: no-commit-to-branch + name: "🌳 git · Protect main branches" + args: ["--branch", "main", "--branch", "master"] + - id: check-vcs-permalinks + name: "🌳 git · Validate VCS links" + + # Filesystem and naming validation + - id: check-case-conflict + name: "📁 filesystem · Check case sensitivity" + - id: check-illegal-windows-names + name: "📁 filesystem · Validate Windows names" + - id: check-symlinks + name: "📁 filesystem · Check symlink validity" + - id: destroyed-symlinks + name: "📁 filesystem · Detect broken symlinks" + + # File format validation - id: check-toml - - id: debug-statements - # Makes sure files end in a newline and only a newline; + name: "📋 format · Validate TOML" + - id: check-yaml + name: "📋 format · Validate YAML" + exclude: conda.recipe/meta.yaml + + # File content fixes + - id: fix-byte-order-marker + name: "✨ fix · Remove BOM markers" - id: end-of-file-fixer + name: "✨ fix · Ensure final newline" - id: mixed-line-ending - # Trims trailing whitespace. Allow a single space on the end of .md lines for hard line breaks. + name: "✨ fix · Normalize line endings" - id: trailing-whitespace - args: [ --markdown-linebreak-ext=md ] - # Sort requirements in requirements.txt files. + name: "✨ fix · Trim trailing whitespace" + args: [--markdown-linebreak-ext=md] - id: requirements-txt-fixer - # Prevent committing directly to trunk - - id: no-commit-to-branch - args: [ "--branch=master" ] - # Detects the presence of private keys + name: "✨ fix · Sort requirements" + + # Security checks - id: detect-private-key + name: "🔒 security · Detect private keys" + # Git commit quality - repo: https://github.com/jorisroovers/gitlint rev: v0.19.1 hooks: - id: gitlint + name: "🌳 git · Validate commit format" + - repo: https://github.com/commitizen-tools/commitizen + rev: v4.11.1 + hooks: + - id: commitizen + name: "🌳 git · Validate commit message" + stages: [commit-msg] + + # Security scanning (grouped together) - repo: https://github.com/Yelp/detect-secrets rev: v1.5.0 hooks: - - id: detect-secrets # Detect accidentally committed secrets + - id: detect-secrets + name: "🔒 security · Detect committed secrets" - - repo: https://github.com/codespell-project/codespell - rev: v2.4.1 + - repo: https://github.com/gitleaks/gitleaks + rev: v8.30.0 hooks: - - id: codespell - args: [--write] - exclude: ^tests + - id: gitleaks + name: "🔒 security · Scan for hardcoded secrets" + - repo: https://github.com/PyCQA/bandit + rev: 1.9.2 + hooks: + - id: bandit + name: "🔒 security · Check Python vulnerabilities" + args: ["-c", "pyproject.toml", "-r", "."] + exclude: ^tests/ + additional_dependencies: [".[toml]"] + +# TODO: Enable it for a single check +# - repo: https://github.com/pypa/pip-audit +# rev: v2.10.0 +# hooks: +# - id: pip-audit +# name: "🔒 security · Audit Python dependencies" +# args: ['--desc', 'on'] + + - repo: https://github.com/semgrep/pre-commit + rev: 'v1.146.0' + hooks: + - id: semgrep + name: "🔒 security · Static analysis (semgrep)" + args: [ '--config=auto', '--error' ] + + + # Spelling and typos + - repo: https://github.com/crate-ci/typos + rev: v1.40.0 + hooks: + - id: typos + name: "📝 spelling · Check typos" + +# - repo: https://github.com/codespell-project/codespell +# rev: v2.4.1 +# hooks: +# - id: codespell +# name: "📝 spelling · Fix common misspellings" +# args: [--write] +# exclude: ^tests + + # CI/CD validation - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.35.0 + rev: 0.36.0 hooks: + - id: check-dependabot + name: "🔧 ci/cd · Validate Dependabot config" - id: check-github-workflows + name: "🔧 ci/cd · Validate GitHub workflows" files: ^\.github/workflows/.*\.ya?ml$ - - repo: https://github.com/akaihola/darker - rev: v3.0.0 - hooks: - - id: darker - additional_dependencies: [black] - + # Python code formatting (order matters: autoflake → pyupgrade → darker/ruff) - repo: https://github.com/PyCQA/autoflake rev: v2.3.1 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/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" + + # Python linting (comprehensive checks) - repo: https://github.com/pycqa/flake8 rev: 7.3.0 hooks: - - id: flake8 + - 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 - # TODO: Remove tests when we will be ready to process tests + - flake8-bugbear + - flake8-comprehensions + - flake8-tidy-imports + - pycodestyle exclude: ^tests - repo: https://github.com/PyCQA/pylint rev: v4.0.4 hooks: - id: pylint + name: "🐍 lint · Check code quality" args: - --exit-zero - - repo: https://github.com/asottile/pyupgrade - rev: v3.21.2 + - repo: https://github.com/dosisod/refurb + rev: v2.2.0 hooks: - - id: pyupgrade - args: [--py310-plus, --keep-runtime-typing] - - - repo: https://github.com/charliermarsh/ruff-pre-commit - # Ruff version. - rev: v0.14.8 - hooks: - # Run the linter. - - id: ruff-check - args: [--fix, --preview, --exit-non-zero-on-fix] - # Run the formatter. - - id: ruff-format + - 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/dosisod/refurb - rev: v2.2.0 - hooks: - - id: refurb + - repo: https://github.com/econchick/interrogate + rev: 1.7.0 + hooks: + - id: interrogate + name: "📝 docs · Check docstring coverage" + args: [ --verbose, --fail-under=53, --ignore-init-method ] + # Python type checking - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.19.0 + rev: v1.19.1 hooks: - - id: mypy + - id: mypy + name: "🐍 types · Check with mypy" args: [--config-file=./pyproject.toml] additional_dependencies: - types-requests @@ -151,27 +277,66 @@ repos: - repo: https://github.com/RobertCraigie/pyright-python rev: v1.1.407 hooks: - - id: pyright + - id: pyright + name: "🐍 types · Check with pyright" - - repo: https://github.com/PyCQA/bandit - rev: 1.9.2 + # Python project configuration + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.24.1 hooks: - - id: bandit - args: ["-c", "pyproject.toml", "-r", "."] - # ignore all tests, not just tests data - exclude: ^tests/ - additional_dependencies: [".[toml]"] + - id: validate-pyproject + name: "🐍 config · Validate pyproject.toml" - - repo: https://github.com/crate-ci/typos - rev: v1.38.1 + - repo: https://github.com/tox-dev/pyproject-fmt + rev: v2.8.0 hooks: - - id: typos + - id: pyproject-fmt + name: "🐍 config · Format pyproject.toml" +# - repo: https://github.com/mgedmin/check-manifest +# rev: '0.50' +# hooks: +# - id: check-manifest +# name: "🐍 📦 packaging · Verify MANIFEST" +# args: [--no-build-isolation, --ignore-bad-ideas=MANIFEST.in] +# additional_dependencies: [setuptools, wheel, setuptools-scm] + + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.10.0 + hooks: + - id: python-check-blanket-noqa + name: "🐍 lint · Disallow blanket noqa" + - id: python-use-type-annotations + name: "🐍 types · Enforce type annotations" + - id: python-check-blanket-type-ignore + name: "🐍 types · Disallow blanket type:ignore" + - id: python-no-log-warn + name: "🐍 lint · Use logging.warning not warn" + - id: text-unicode-replacement-char + name: "📋 format · Detect unicode replacement char" + - id: python-no-eval + name: "🔒 security · Prevent eval() usage" + + # Markdown formatting - repo: https://github.com/executablebooks/mdformat rev: 1.0.0 hooks: - id: mdformat + name: "📝 markdown · Format files" additional_dependencies: - # gfm = GitHub Flavored Markdown - 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" + + # Makefile linting + - repo: https://github.com/checkmake/checkmake + rev: 0.2.2 + hooks: + - id: checkmake + name: "🔧 build · Lint Makefile" diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bb8dda..e7d949f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,55 @@ We [keep a changelog.](http://keepachangelog.com/) ## [Unreleased] +### Added + +- Add Keys and Domain Keys API endpoints: + + - Add `handle_keys` to `mailgun.handlers.keys_handler`. + - Add `handle_dkimkeys` to `mailgun.handlers.domains_handler`. + - Add "dkim" key to special cases in the class `Config`. + +- Examples: + + - Add the `get_dkim_keys()`, `post_dkim_keys()`, `delete_dkim_keys()` examples to `mailgun/examples/domain_examples.py`. + - Add the `get_keys()`, `post_keys()`, `delete_key()`, `regenerate_key()` examples to `mailgun/examples/keys_examples.py`. + +- Docs: + + - Add `Keys` and `Domain Keys` sections with examples to `README.md`. + - Add docstrings to the test class `KeysTests` & `AsyncKeysTests` and their methods. + - Add `CONTRIBUTING.md`. + - Add `MANIFEST.in`. + +- Tests: + + - Add dkim keys tests to `DomainTests` and only `test_get_dkim_keys`, `test_post_dkim_keys_invalid_pem_string` to `AsyncDomainTests`. + - Add classes `KeysTests` and `AsyncKeysTests` to `tests/tests.py`. + - Add keys tests to `KeysTests` and `AsyncKeysTests`. + +- CI: + + - Add more pre-commit hooks. + +### Changed + +- Update `get_own_user_details()` by creating `client_with_secret_key` in `mailgun/examples/users_examples.py`. +- Improve the users' example in `README.md`. +- Fix markdown structure in `README.md`. +- Update environment variables in `README.md`. +- Move `BounceClassificationTests` to another place in `tests/tests.py`. +- Replace some pytest's skip marks with xfail. +- Disable `codespell` pre-commit hook as it lashes with `typos`. +- Update `pre-commit` hooks to the latest versions. +- Update test dependencies: add `openssl` and `pytest-asyncio` to `environment-dev.yaml` and `pyproject.toml`. +- Add `.server.key` to `.gitignore`. +- Add a constraint `py<311` for `typing_extensions >=4.7.1` in files `environment.yaml`, `environment-dev.yaml`, `pyproject.toml`, and in `mailgun/client.py`. +- Improve `pyproject.toml`. + +### Pull Requests Merged + +- [PR_27](https://github.com/mailgun/mailgun-python/pull/27) - Add Keys and Domain Keys API endpoints + ## [1.5.0] - 2025-12-11 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..27b0e47 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,24 @@ +### Contributing + +First off, thank you for considering contributing to Mailgun Python SDK. Contributions to the `mailgun-python` repository from the community are welcome and encouraged! + +Mailgun loves developers. You can be part of this project! + +This Python SDK is a great introduction to the open source world, check out the code! + +Please create issues for any major changes so it can be discussed first. For small bug fixes feel free to submit the PR directly. + +Make sure to run tests before creating the PR. + +When submitting a pull request, please include the purpose and implementation details. + +Feel free to ask anything, and contribute: + +- Fork the project. +- Create a new branch. +- Implement your feature or bug fix. +- Add documentation to it. +- Commit, push, open a pull request and voila. + +If you have suggestions on how to improve the guides, please submit an issue in our +[Official API Documentation](https://documentation.mailgun.com). diff --git a/LICENSE b/LICENSE index 6c01456..261eeb9 100644 --- a/LICENSE +++ b/LICENSE @@ -199,4 +199,3 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ->>>>>>> 6b1e6b18e0fd97cff8c7a65813d573875c1cdf0d diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..28f7849 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,30 @@ +# MANIFEST.in +# Include critical files for building/installing +include LICENSE +include README.md +include pyproject.toml +include *.md +include *.typed +include *.yaml +include Makefile +include MANIFEST.in + +# Include package data +recursive-include mailgun *.py +recursive-include mailgun *.csv +recursive-include mailgun *.mime +recursive-include mailgun *.txt + +# Exclude development/CI files +exclude .editorconfig +exclude .pylintrc +exclude .pre-commit-config.yaml + +# Exclude conda-specific files (not needed in PyPI sdist) +prune conda.recipe + +# Exclude test files (optional - include if you want tests in sdist) +prune tests + +# Exclude CI/CD configs +prune .github diff --git a/Makefile b/Makefile index 9092db1..7e5a879 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: clean clean-env clean-test clean-pyc clean-build clean-other help dev test test-debug test-cov pre-commit lint format format-docs analyze docs +.PHONY: all clean clean-env clean-test clean-pyc clean-build clean-other help dev test test-debug test-cov pre-commit lint format format-docs analyze docs .DEFAULT_GOAL := help # The `.ONESHELL` and setting `SHELL` allows us to run commands that require diff --git a/README.md b/README.md index 1b97d3b..eb21106 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ Check out all the resources and Python code examples in the official - [Overview](#overview) - [Base URL](#base-url) - [Authentication](#authentication) + - [Client](#client) + - [AsyncClient](#asyncclient) - [API Response Codes](#api-response-codes) - [Request examples](#request-examples) - [Full list of supported endpoints](#full-list-of-supported-endpoints) @@ -37,6 +39,8 @@ Check out all the resources and Python code examples in the official - [Update a domain](#update-a-domain) - [Domain connections](#domain-connections) - [Domain keys](#domain-keys) + - [List keys for all domains](#list-keys-for-all-domains) + - [Create a domain key](#create-a-domain-key) - [Update DKIM authority](#update-dkim-authority) - [Domain Tracking](#domain-tracking) - [Get tracking settings](#get-tracking-settings) @@ -100,7 +104,7 @@ Check out all the resources and Python code examples in the official - [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](#) + - [Get a user's details](#get-a-users-details) - [License](#license) - [Contribute](#contribute) - [Contributors](#contributors) @@ -234,8 +238,14 @@ export DOMAINS_DEDICATED_IP="127.0.0.1" export MAILLIST_ADDRESS="everyone@mailgun.domain.com" export VALIDATION_ADDRESS_1="test1@i.ua" export VALIDATION_ADDRESS_2="test2@gmail.com" +export MAILGUN_EMAIL="username@example.com" +export USER_ID="123456789012345678901234" +export USER_NAME="Name Surname" +export ROLE="admin" ``` +### Client + Initialize your [Mailgun](http://www.mailgun.com/) client: ```python @@ -501,6 +511,68 @@ def get_connections() -> None: #### Domain keys +### List keys for all domains + +List domain keys, and optionally filter by signing domain or selector. The page & limit data is only required when paging through the data. + +```python +def get_dkim_keys() -> None: + """ + GET /v1/dkim/keys + :return: + """ + data = { + "page": "string", + "limit": "0", + "signing_domain": "python.test.domain5", + "selector": "smtp", + } + + request = client.dkim_keys.get(data=data) + print(request.json()) +``` + +#### Create a domain key + +Create a domain key. +Note that once private keys are created or imported they are never exported. +Alternatively, you can import an existing PEM file containing a RSA private key in PKCS #1, ASn.1 DER format. +Note, the pem can be passed as a file attachment or as a form-string parameter. + +```python +def post_dkim_keys() -> None: + """ + POST /v1/dkim/keys + :return: + """ + import subprocess + from pathlib import Path + + # 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"]) + + files = [ + ( + "pem", + ("server.key", Path(".server.key").read_bytes()), + ) + ] + + data = { + "signing_domain": "python.test.domain5", + "selector": "smtp", + "bits": "2048", + "pem": files, + } + + headers = {"Content-Type": "multipart/form-data"} + + request = client.dkim_keys.create(data=data, headers=headers, files=files) + print(request.json()) +``` + ##### Update DKIM authority ```python @@ -941,9 +1013,7 @@ def import_list_unsubs() -> None: :return: """ files = { - "unsubscribe2_csv": Path( - "mailgun/doc_tests/files/mailgun_unsubscribes.csv" - ).read_bytes() + "unsubscribe2_csv": Path("mailgun/doc_tests/files/mailgun_unsubscribes.csv").read_bytes() } req = client.unsubscribes_import.create(domain=domain, files=files) print(req.json()) @@ -976,11 +1046,7 @@ def import_complaint_list() -> None: POST //complaints/import, Content-Type: multipart/form-data :return: """ - files = { - "complaints_csv": Path( - "mailgun/doc_tests/files/mailgun_complaints.csv" - ).read_bytes() - } + files = {"complaints_csv": Path("mailgun/doc_tests/files/mailgun_complaints.csv").read_bytes()} req = client.complaints_import.create(domain=domain, files=files) print(req.json()) ``` @@ -1165,9 +1231,7 @@ def get_all_versions() -> None: GET //templates/