From cb0e474bd422291e069e0cdbddf11b07d4e06763 Mon Sep 17 00:00:00 2001 From: skupriienko <61395455+skupriienko@users.noreply.github.com> Date: Wed, 7 May 2025 12:51:32 +0300 Subject: [PATCH] Release 1.4.0 (#117) * Improve CI Automation and package management (#116) * ci: Fix conda.recipe, add samples * ci: Disable TestPyPI job; Add samples to stdist * docs: Update CHANGELOG, fix Python support versions in README * docs: Fix urls * docs: Update badges * Release v1.4.0 --- .gitattributes | 14 ++ .github/ISSUE_TEMPLATE/bug_report.yml | 73 +++++++ .github/ISSUE_TEMPLATE/documentation.yml | 32 +++ .github/ISSUE_TEMPLATE/feature_request.yml | 57 ++++++ .github/workflows/commit_checks.yaml | 13 +- .github/workflows/issue-triage.yml | 47 +++++ .github/workflows/pr_validation.yml | 28 +++ .github/workflows/publish.yml | 84 ++++++++ .pre-commit-config.yaml | 67 +++---- .travis.yml | 23 --- CHANGELOG.md | 128 ++++++++---- Makefile | 7 +- README.md | 218 +++++++++++++-------- conda.recipe/meta.yaml | 17 +- environment-dev.yaml | 42 ++-- mailjet_rest/_version.py | 1 + mailjet_rest/utils/version.py | 27 ++- pyproject.toml | 72 ++++++- test.py | 2 +- 19 files changed, 717 insertions(+), 235 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/documentation.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/workflows/issue-triage.yml create mode 100644 .github/workflows/pr_validation.yml create mode 100644 .github/workflows/publish.yml delete mode 100644 .travis.yml create mode 100644 mailjet_rest/_version.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8c24181 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +# Automatically detect text files and perform LF normalization +* text=auto + +# Source files should have Unix line endings +*.py text eol=lf +*.md text eol=lf +*.toml text eol=lf +*.yaml text eol=lf +*.yml text eol=lf + +# Exclude some files from exporting +.gitattributes export-ignore +.gitignore export-ignore +.github export-ignore diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..90b657b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,73 @@ +name: 🐛 Bug Report +description: Create a bug report. +title: "[Bug]: " +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: input + id: version + attributes: + label: Version + description: What version of our package are you running? + placeholder: ex. 1.0.0 + validations: + required: true + - type: dropdown + id: os + attributes: + label: Operating System + description: What operating system are you using? + options: + - Windows + - macOS + - Linux + - Other + validations: + required: true + - type: dropdown + id: python-version + attributes: + label: Python Version + description: What Python version are you using? + options: + - '3.9' + - '3.10' + - '3.11' + - '3.12' + - '3.13' + validations: + required: true + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you see! + value: "A bug happened!" + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Steps to reproduce + description: How can we reproduce this issue? + placeholder: | + 1. Install package '...' + 2. Run command '...' + 3. See error + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code. + render: shell + - type: textarea + id: additional + attributes: + label: Additional information + description: Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml new file mode 100644 index 0000000..13365b8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -0,0 +1,32 @@ +name: Documentation +description: Create a documentation-related issue. +labels: + - type::documentation +body: + - type: markdown + attributes: + value: | + > [!NOTE] + > Documentation requests that are incomplete or missing information may be closed as inactionable. + - type: checkboxes + id: checks + attributes: + label: Checklist + description: Please confirm and check all of the following options. + options: + - label: I added a descriptive title + required: true + - label: I searched open reports and couldn't find a duplicate + required: true + - type: textarea + id: what + attributes: + label: What happened? + description: Mention here any typos, broken links, or missing, incomplete, or outdated information that you have noticed in the docs. + validations: + required: true + - type: textarea + id: context + attributes: + label: Additional Context + description: Include any additional information (or screenshots) that you think would be valuable. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..2cf6658 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,57 @@ +name: 🚀 Feature Request +description: Suggest an idea for this project +title: "[Feature]: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to suggest a new feature! + + > [!NOTE] + > Feature requests that are incomplete or missing information may be closed as inactionable. + - type: checkboxes + id: checks + attributes: + label: Checklist + description: Please confirm and check all of the following options. + options: + - label: I added a descriptive title + required: true + - label: I searched open requests and couldn't find a duplicate + required: true + - type: textarea + id: problem + attributes: + label: Is your feature request related to a problem? + description: A clear and concise description of the problem. Ex. I'm always frustrated when [...] + validations: + required: true + - type: textarea + id: solution + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + - type: dropdown + id: priority + attributes: + label: Priority + description: How important is this feature to you? + options: + - Low (nice to have) + - Medium + - High (would significantly improve my workflow) + validations: + required: true diff --git a/.github/workflows/commit_checks.yaml b/.github/workflows/commit_checks.yaml index 3993ccd..10a1a83 100644 --- a/.github/workflows/commit_checks.yaml +++ b/.github/workflows/commit_checks.yaml @@ -13,6 +13,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 + with: + python-version: '3.12' # Specify a Python version explicitly - uses: pre-commit/action@v3.0.1 test: @@ -25,13 +27,14 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest", "macos-latest", "windows-latest"] - python-version: ["3.9", "3.10", "3.11", "3.12"] - #environment: mailjet + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] env: MJ_APIKEY_PUBLIC: ${{ secrets.MJ_APIKEY_PUBLIC }} MJ_APIKEY_PRIVATE: ${{ secrets.MJ_APIKEY_PRIVATE }} steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Get full history with tags (required for setuptools-scm) - uses: conda-incubator/setup-miniconda@v3 with: python-version: ${{ matrix.python-version }} @@ -41,7 +44,7 @@ jobs: - name: Install the package run: | - pip install -e . + pip install . conda info - name: Test package imports - run: python -c "import mailjet_rest; print('mailjet_rest version is', mailjet_rest.utils.version.get_version())" + run: python -c "import mailjet_rest" diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml new file mode 100644 index 0000000..e500793 --- /dev/null +++ b/.github/workflows/issue-triage.yml @@ -0,0 +1,47 @@ +name: Issue Triage + +on: + issues: + types: [opened, labeled, unlabeled, reopened] + +jobs: + triage: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Initial triage + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const issue = context.payload.issue; + // Check if this is a bug report + if (issue.title.includes('[Bug]')) { + // Add priority labels based on content + if (issue.body.toLowerCase().includes('crash') || + issue.body.toLowerCase().includes('data loss')) { + github.rest.issues.addLabels({ + issue_number: issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['priority: high'] + }); + } + // Assign to bug team + github.rest.issues.addAssignees({ + issue_number: issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + assignees: [''] + }); + } + // Check if this is a feature request + if (issue.title.includes('[Feature]')) { + github.rest.issues.addLabels({ + issue_number: issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: ['needs-review'] + }); + } diff --git a/.github/workflows/pr_validation.yml b/.github/workflows/pr_validation.yml new file mode 100644 index 0000000..a699c1f --- /dev/null +++ b/.github/workflows/pr_validation.yml @@ -0,0 +1,28 @@ +name: PR Validation + +on: + pull_request: + branches: [main] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Build package + run: | + pip install --upgrade build setuptools wheel setuptools-scm + python -m build + + - name: Test installation + run: | + pip install dist/*.whl + python -c "from importlib.metadata import version; print(version('mailjet_rest'))" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..14b1b1c --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,84 @@ +name: Publish Package + +on: + push: + tags: ['v*'] # Triggers on any tag push + release: + types: [published] # Triggers when a GitHub release is published + workflow_dispatch: # Manual trigger + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + id-token: write # Required for trusted publishing + contents: read + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install build tools + run: pip install --upgrade build setuptools wheel setuptools-scm twine + + - name: Extract version + id: get_version + run: | + # Get clean version from the tag or release + if [[ "${{ github.event_name }}" == "release" ]]; then + # For releases, get the version from the release tag + TAG_NAME="${{ github.event.release.tag_name }}" + else + # For tags, get version from the tag + TAG_NAME="${{ github.ref_name }}" + fi + + # Remove 'v' prefix + VERSION=$(echo $TAG_NAME | sed 's/^v//') + + # Check if this is a stable version (no rc, alpha, beta, dev, etc.) + if [[ $TAG_NAME =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "IS_STABLE=true" >> $GITHUB_ENV + else + echo "IS_STABLE=false" >> $GITHUB_ENV + fi + + echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Build package + run: | + # Force clean version + export SETUPTOOLS_SCM_PRETEND_VERSION=$VERSION + python -m build + + - name: Check dist + run: | + ls -alh + twine check dist/* + + # Always publish to TestPyPI for all tags and releases + # TODO: Enable it later. +# - name: Publish to TestPyPI +# uses: pypa/gh-action-pypi-publish@release/v1 +# with: +# repository-url: https://test.pypi.org/legacy/ +# password: ${{ secrets.TEST_PYPI_API_TOKEN }} +# skip-existing: true +# verbose: true + + # Only publish to PyPI for stable GitHub releases (no RC/alpha/beta) + - name: Publish to PyPI + # TODO: Enable '&& env.IS_STABLE == 'true' only publish to PyPI for stable GitHub releases (no RC/alpha/beta) + if: github.event_name == 'release' #&& env.IS_STABLE == 'true' + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + verbose: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9eaf0e5..2758d73 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,14 +32,11 @@ repos: # git-lfs rather than committing them directly to the git history - id: check-added-large-files args: [ "--maxkb=500" ] - - id: fix-byte-order-marker - - id: check-case-conflict # Fails if there are any ">>>>>" lines in files due to merge conflicts. - id: check-merge-conflict # ensure syntaxes are valid - id: check-toml - id: debug-statements - - id: detect-private-key # Makes sure files end in a newline and only a newline; - id: end-of-file-fixer - id: mixed-line-ending @@ -60,18 +57,19 @@ repos: - id: gitlint - repo: https://github.com/codespell-project/codespell - rev: v2.3.0 + rev: v2.4.1 hooks: - id: codespell args: [--write] + exclude: ^tests - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.31.0 + rev: 0.33.0 hooks: - id: check-github-workflows - - repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v2.0.4 + - repo: https://github.com/hhatto/autopep8 + rev: v2.3.2 hooks: - id: autopep8 exclude: ^docs/ @@ -92,7 +90,7 @@ repos: - --ignore-init-module-imports - repo: https://github.com/pycqa/flake8 - rev: 7.1.1 + rev: 7.2.0 hooks: - id: flake8 additional_dependencies: @@ -103,7 +101,7 @@ repos: - repo: https://github.com/PyCQA/pylint - rev: v3.3.3 + rev: v3.3.7 hooks: - id: pylint args: @@ -113,11 +111,11 @@ repos: rev: v3.19.1 hooks: - id: pyupgrade - args: [--py38-plus, --keep-runtime-typing] + args: [--py39-plus, --keep-runtime-typing] - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: v0.9.2 + rev: v0.11.8 hooks: # Run the linter. - id: ruff @@ -131,7 +129,7 @@ repos: - id: refurb - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.14.1 + rev: v1.15.0 hooks: - id: mypy args: @@ -141,12 +139,12 @@ repos: exclude: ^samples/ - repo: https://github.com/RobertCraigie/pyright-python - rev: v1.1.392.post0 + rev: v1.1.400 hooks: - id: pyright - repo: https://github.com/PyCQA/bandit - rev: 1.8.2 + rev: 1.8.3 hooks: - id: bandit args: ["-c", "pyproject.toml", "-r", "."] @@ -155,36 +153,17 @@ repos: additional_dependencies: [".[toml]"] - repo: https://github.com/crate-ci/typos - rev: dictgen-v0.3.1 + # Important: Keep an exact version (not v1) to avoid pre-commit issues + # after running 'pre-commit autoupdate' + rev: v1.31.1 hooks: - id: typos -# - repo: https://github.com/google/yapf -# rev: v0.40.2 -# hooks: -# - id: yapf -# name: "yapf" -# additional_dependencies: [toml] - -# - repo: https://github.com/mattseymour/pre-commit-pytype -# rev: '2023.5.8' -# hooks: -# - id: pytype - -# - repo: https://github.com/executablebooks/mdformat -# rev: 0.7.17 -# hooks: -# - id: mdformat -# additional_dependencies: -# # gfm = GitHub Flavored Markdown -# - mdformat-gfm -# - mdformat-black - -# - repo: https://github.com/adrienverge/yamllint.git -# rev: v1.35.1 -# hooks: -# - id: yamllint -# #args: [--strict] -# entry: yamllint -# language: python -# types: [ file, yaml ] + - repo: https://github.com/executablebooks/mdformat + rev: 0.7.22 + hooks: + - id: mdformat + additional_dependencies: + # gfm = GitHub Flavored Markdown + - mdformat-gfm + - mdformat-black diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 315ac8f..0000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ ---- -language: python -python: -- '2.7' -- '3.5' -- '3.8' -deploy: - provider: pypi - user: api.mailjet - password: - secure: gI8oqHszMz4dTfs8g6aKKZgOMN04HK26mPrrsO2iz6gsUB1b7zM8RwLSWCPH2YjmWaAF9Bx5VrQ6YayLHX3svRWUsWDgBGGCcsogKEfIEmMasKMnjGrVJRS5fb0j/r9d4qXaITdoWeRh7/HBdv6u0gAwMV/fyrT6XiiDwWxbYfyLwbJx9IH79Gkyh6Xwq8nkexd5oQDybE0IQ94Q9/6t0CHbmCLrTinR1q0diV/bKK5+zSqiCmatzIq5kMre32iurs4LPZ4QTWJkQqZSh4UzQAJ42hjH8w+YrDxB4kRjOraqDNKHnr06dcOUFK9bJ0/wZU2MJh0II3hrOo2MUphhChcrKQDWuyCPTcZhDUxVBhcy0wWz3RfxXZ442VxHpCrNyNesxmJZZc9AxMfzerQvCIFGeeJCTunUz5I245FNIsnIqDJ5IiM9Ukn/WIfHsqh5fVVejQotfl6YVmlJ+nmxNQnBIXPWlpg4CXJN/TvFTjLsPeWxGZZJKrMGsVmEqHgtH+31pn/ec6frXbx8cDDcoAteEantQWcM3AJj32BL8mA8RpwWONKfFNKFpWyCtoTq+ERbWjv7EvKgudLqX50WPi2uaGFp8Ik6tWAi7hcgNYneyAhOHv7FZJf582n1dTSB0Fv4izphsmJKjIC02EGXYtKcDFA0m1zWLuqLavuFFO0= - on: - tags: true - repo: mailjet/mailjet-apiv3-python -install: pip install -r requirements.txt -script: python test.py -notifications: - slack: - secure: Y6dTB+/gVfUn7z84et8HAS8gb21+FJoeDmz9f6Lkc9VUk8fL5lqrsV5cZedEWH1sXzzbN8GN2qOoLDKFa+HpxxA/27urSSVKYxYzqdZQZtVZBRSjVzflDJAUzUk1zfn1SmkhMJZO+EF7SOGLBSqbR2LgIzac6P6lKP6dtI8ZjS++O8VtaPOWPzxb8AuByOO6YR7sl6ZWO3OK30ovEOnAAjeiAC8nD0isjZylzhABQj2AKSelFf0zczMTJBMlB8gIXh+gf+sIG4RZknDb2qnFxstHU8p8FD54KdfkOA4W8UTGBc+5DUx0z8fVIo9JcBwIbbFIlcvZ5LY7atu7baBQO7jURXk7uI/w9gDFoE+NhrqF06NqeCHgySc3KSNf2NaxCiKSD5K4WDxumV16KliMqJzjumwG08+/TqgIzC9/Aj/b+5skxzhWkRP/H5iz8sOqCPHGk1pk7B1PwxuOHwIuzeLQ9aELYQgFIBVMqM2bFLLRk9eRKwpkpagApyAhTjV3hqAmUanL6fiPqIar0f4QQ5vFFronFCp08hVQ/b+WA8cLJ6r3FqBiP0XUzea8gsdbgXO4HB/hXtWx7oWzuPL39kx+aJHdt/rSI90shkk7dzhI3FQy+iSC5QnfVhNPyc3OprzWNlNcZjbI7ElS106LjrKcr8ilAMgpuFa06wuybxo= -env: - global: - - secure: x2agpUFUHBYcTgmuHeYuqxuNIMFz1V0PG9a09BpDmYrj0PT2BsecaMCtsWyWOUkzTb9S2/fE6oJnWjyNewp936px681K1aBa5GKKCkifnbxkRRY0dmUkFcrgWG8JZH9hXjzmgjUzwTefJ+xKkeVCySRCNgFm6MP0TmzKZWHjAxOHIHz1akJBfuAhCyVJU0grw7JSAPHSwMj/7erj7ub1pEvWjRjtJe9pMYTtrSYJRtt2qCkSRNK3/i+YZ7mKfigcaIcDgdaPYw0osUqB1DZHJK4RtC80pHkZNAosWIIMU7WohpYijGTIZRPjY4le3uiGqTJcaDhL84Jnmervczd62Iqhf9TE6YfKYksnfbk4YYy9GcTPggBSUV0COB9vApncVENZgeFUO8nSodL0ru5PO0KHcX6Er2zMdqieKseuJY1iLDidyqbTHqR9bZ9dck9CLA4KfLUycwaXYHb9c35iA9caOn+Xm7G1UvVSh3uE63FBsG1ubATe6pIfWZFgVo6zbH52hOTXQhLeximCD7CiSPef69iNzH9jTi9LU04hTuk69X8BWqvHTiYwPoLhdQueXwy2/LaOvSGex8qS5cGYBnWkeb0y99Qm6WpQjCkZhmcDvSXBlBnbbhm6Xc4NnLdX8cp/nVBYrYsgVsf2T7vUnmJBngv1Vsqa4+r9QRxBbxk= - - secure: Dwmug98uIhT3i03nai2Ufa0fBJS4i4zYclqywZJkqEVS84UrKDTZOLJFCDgJv3NlZ29BxvrnH8QqPHn9J6hJb++DGT6Ak7vonfuMYedxNAADn/RjBun+esQsPQYdko/GwGw1Z6kPucBT6Jp3Owd+GDjbnTXFmwSfag/sbTc38lp3mgvDAvcUiyTmQD0hsbHLw6GrpRte9BpSfnJ5ImCgz9nacuZPC084FQcMi0PyV4Ug9L32jnfVePwFD1pCjZpc41m3M2SP89XBjUrBRwyDzgTS8jxt82LN3mQKpxl3EguWLYuKCrK0vJQphrWlhUcRdAkCroSgTtWa9MAGb1DT44OqcbAGEPcMqJKNR7MtzsFnPq6QLO5IDXn05OmOZi1BChxXpENq/gmj2c8OmPkndZTHBwfG3sP0iuC7xrlSBr/++UWQYi07oIYgWfpxYMnXUS0ZEHOUg4oCvQBydB+mcVgRinN2cgXzwfOoVvAofjuT52mzyMHJz9OBqbcyqnCdEZzxS8cn7LTf6ZoWrFFSDQg5Fn8G/6U+yzPT87iz3di0uuCU7VsltXMs25HXdLLQxxbqBkPOhawtIUuzxmq/1eLBb0R+GzjgAsqMJl40iJdnDiYaOAgb6LE6Ix7II80I5nfLkE/60N0AgzZHJy6c/RhYy39eI7D9HQBDLcyW8AM= diff --git a/CHANGELOG.md b/CHANGELOG.md index 0471a47..6f9468a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,32 +1,73 @@ -# Changelog +# CHANGELOG -## [Unreleased](https://github.com/mailjet/mailjet-apiv3-python/tree/HEAD) +We [keep a changelog.](http://keepachangelog.com/) -[Full Changelog](https://github.com/mailjet/mailjet-apiv3-python/compare/v1.3.2...HEAD) +## [Unreleased] + +## [1.4.0] - 2025-05-07 + +### Added + +- Enabled debug logging +- Support for Python >=3.9,\<3.14 +- CI Automation (commit checks, issue-triage, PR validation, publish) +- Issue templates for bug report, feature request, documentation +- Type hinting +- Docstrings +- A conda recipe (meta.yaml) +- Package management stuff: pyproject.toml, .editorconfig, .gitattributes, .gitignore, .pre-commit-config.yaml, Makefile, environment-dev.yaml, environment.yaml +- Linting: py.typed +- New samples +- New tests + +### Changed + +- Update README.md +- Improved tests + +### Removed + +- requirements.txt and setup.py are replaced by pyproject.toml +- .travis.yml was obsolete + +### Pull Requests Merged + +- [PR_105](https://github.com/mailjet/mailjet-apiv3-python/pull/105) - Update README.md, fix the license name in setup.py +- [PR_107](https://github.com/mailjet/mailjet-apiv3-python/pull/107) - PEP8 enabled +- [PR_108](https://github.com/mailjet/mailjet-apiv3-python/pull/108) - Support py>=39,\=3.9,\<3.14 -It's tested up to 3.12 (including). +It's tested up to 3.13 (including). ## Requirements -### Build backend +### Build backend dependencies -To build the `mailjet_rest` package you need `setuptools` (as a build backend) and `wheel`. +To build the `mailjet_rest` package from the sources you need `setuptools` (as a build backend), `wheel`, and `setuptools-scm`. -### Runtime +### Runtime dependencies -At runtime the package requires only `requests`. +At runtime the package requires only `requests >=2.32.3`. + +### Test dependencies + +For running test you need `pytest >=7.0.0` at least. +Make sure to provide the environment variables from [Authentication](#authentication). ## Installation -Use the below code to install the wrapper: +### pip install + +Use the below code to install the the wrapper: + +```bash +pip install mailjet-rest +``` + +#### git clone & pip install locally -``` bash +Use the below code to install the wrapper locally by cloning this repository: + +```bash git clone https://github.com/mailjet/mailjet-apiv3-python cd mailjet-apiv3-python ``` -``` bash +```bash pip install . ``` -or using `conda` and `make` on Unix platforms: +#### conda & make + +Use the below code to install it locally by `conda` and `make` on Unix platforms: -``` bash +```bash make install ``` ### For development +#### Using conda + on Linux or macOS: - A basic environment with a minimum number of dependencies: -``` bash +```bash make dev conda activate mailjet ``` - A full dev environment: -``` bash +```bash make dev-full conda activate mailjet-dev ``` @@ -107,7 +137,7 @@ export MJ_APIKEY_PUBLIC='your api key' export MJ_APIKEY_PRIVATE='your api secret' ``` -Initialize your [Mailjet][mailjet] client: +Initialize your [Mailjet] client: ```python # import the mailjet wrapper @@ -115,8 +145,8 @@ from mailjet_rest import Client import os # Get your environment Mailjet keys -api_key = os.environ['MJ_APIKEY_PUBLIC'] -api_secret = os.environ['MJ_APIKEY_PRIVATE'] +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] mailjet = Client(auth=(api_key, api_secret)) ``` @@ -128,16 +158,17 @@ Here's an example on how to send an email: ```python from mailjet_rest import Client import os -api_key = os.environ['MJ_APIKEY_PUBLIC'] -api_secret = os.environ['MJ_APIKEY_PRIVATE'] + +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] mailjet = Client(auth=(api_key, api_secret)) data = { - 'FromEmail': '$SENDER_EMAIL', - 'FromName': '$SENDER_NAME', - 'Subject': 'Your email flight plan!', - 'Text-part': 'Dear passenger, welcome to Mailjet! May the delivery force be with you!', - 'Html-part': '

Dear passenger, welcome to Mailjet!
May the delivery force be with you!', - 'Recipients': [{'Email': '$RECIPIENT_EMAIL'}] + "FromEmail": "$SENDER_EMAIL", + "FromName": "$SENDER_NAME", + "Subject": "Your email flight plan!", + "Text-part": "Dear passenger, welcome to Mailjet! May the delivery force be with you!", + "Html-part": '

Dear passenger, welcome to Mailjet!
May the delivery force be with you!', + "Recipients": [{"Email": "$RECIPIENT_EMAIL"}], } result = mailjet.send.create(data=data) print(result.status_code) @@ -156,16 +187,16 @@ The Mailjet API is spread among three distinct versions: Since most Email API endpoints are located under `v3`, it is set as the default one and does not need to be specified when making your request. For the others you need to specify the version using `version`. For example, if using Send API `v3.1`: -``` python +```python # import the mailjet wrapper from mailjet_rest import Client import os # Get your environment Mailjet keys -api_key = os.environ['MJ_APIKEY_PUBLIC'] -api_secret = os.environ['MJ_APIKEY_PRIVATE'] +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] -mailjet = Client(auth=(api_key, api_secret), version='v3.1') +mailjet = Client(auth=(api_key, api_secret), version="v3.1") ``` For additional information refer to our [API Reference](https://dev.mailjet.com/reference/overview/versioning/). @@ -175,7 +206,7 @@ For additional information refer to our [API Reference](https://dev.mailjet.com/ The default base domain name for the Mailjet API is `api.mailjet.com`. You can modify this base URL by setting a value for `api_url` in your call: ```python -mailjet = Client(auth=(api_key, api_secret),api_url="https://api.us.mailjet.com/") +mailjet = Client(auth=(api_key, api_secret), api_url="https://api.us.mailjet.com/") ``` If your account has been moved to Mailjet's **US architecture**, the URL value you need to set is `https://api.us.mailjet.com`. @@ -188,9 +219,7 @@ For example, to reach `statistics/link-click` path you should call `statistics_l ```python # GET `statistics/link-click` mailjet = Client(auth=(api_key, api_secret)) -filters = { - 'CampaignId': 'xxxxxxx' -} +filters = {"CampaignId": "xxxxxxx"} result = mailjet.statistics_linkClick.get(filters=filters) print(result.status_code) print(result.json()) @@ -198,6 +227,11 @@ print(result.json()) ## Request examples +### Full list of supported endpoints + +> [!IMPORTANT]\ +> This is a full list of supported endpoints this wrapper provides [samples](samples) + ### POST request #### Simple POST request @@ -206,14 +240,14 @@ print(result.json()) """ Create a new contact: """ + from mailjet_rest import Client import os -api_key = os.environ['MJ_APIKEY_PUBLIC'] -api_secret = os.environ['MJ_APIKEY_PRIVATE'] + +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] mailjet = Client(auth=(api_key, api_secret)) -data = { - 'Email': 'Mister@mailjet.com' -} +data = {"Email": "Mister@mailjet.com"} result = mailjet.contact.create(data=data) print(result.status_code) print(result.json()) @@ -225,23 +259,19 @@ print(result.json()) """ Manage the subscription status of a contact to multiple lists: """ + from mailjet_rest import Client import os -api_key = os.environ['MJ_APIKEY_PUBLIC'] -api_secret = os.environ['MJ_APIKEY_PRIVATE'] + +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] mailjet = Client(auth=(api_key, api_secret)) -id = '$ID' +id = "$ID" data = { - 'ContactsLists': [ - { - "ListID": "$ListID_1", - "Action": "addnoforce" - }, - { - "ListID": "$ListID_2", - "Action": "addforce" - } - ] + "ContactsLists": [ + {"ListID": "$ListID_1", "Action": "addnoforce"}, + {"ListID": "$ListID_2", "Action": "addforce"}, + ] } result = mailjet.contact_managecontactslists.create(id=id, data=data) print(result.status_code) @@ -256,10 +286,12 @@ print(result.json()) """ Retrieve all contacts: """ + from mailjet_rest import Client import os -api_key = os.environ['MJ_APIKEY_PUBLIC'] -api_secret = os.environ['MJ_APIKEY_PRIVATE'] + +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] mailjet = Client(auth=(api_key, api_secret)) result = mailjet.contact.get() print(result.status_code) @@ -272,13 +304,15 @@ print(result.json()) """ Retrieve all contacts that are not in the campaign exclusion list: """ + from mailjet_rest import Client import os -api_key = os.environ['MJ_APIKEY_PUBLIC'] -api_secret = os.environ['MJ_APIKEY_PRIVATE'] + +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] mailjet = Client(auth=(api_key, api_secret)) filters = { - 'IsExcludedFromCampaigns': 'false', + "IsExcludedFromCampaigns": "false", } result = mailjet.contact.get(filters=filters) print(result.status_code) @@ -317,12 +351,14 @@ print(result.json()) """ Retrieve a specific contact ID: """ + from mailjet_rest import Client import os -api_key = os.environ['MJ_APIKEY_PUBLIC'] -api_secret = os.environ['MJ_APIKEY_PRIVATE'] + +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] mailjet = Client(auth=(api_key, api_secret)) -id_ = 'Contact_ID' +id_ = "Contact_ID" result = mailjet.contact.get(id=id_) print(result.status_code) print(result.json()) @@ -338,23 +374,19 @@ Here's an example of a `PUT` request: """ Update the contact properties for a contact: """ + from mailjet_rest import Client import os -api_key = os.environ['MJ_APIKEY_PUBLIC'] -api_secret = os.environ['MJ_APIKEY_PRIVATE'] + +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] mailjet = Client(auth=(api_key, api_secret)) -id_ = '$CONTACT_ID' +id_ = "$CONTACT_ID" data = { - 'Data': [ - { - "Name": "first_name", - "value": "John" - }, - { - "Name": "last_name", - "value": "Smith" - } - ] + "Data": [ + {"Name": "first_name", "value": "John"}, + {"Name": "last_name", "value": "Smith"}, + ] } result = mailjet.contactdata.update(id=id_, data=data) print(result.status_code) @@ -371,17 +403,23 @@ Here's an example of a `DELETE` request: """ Delete an email template: """ + from mailjet_rest import Client import os -api_key = os.environ['MJ_APIKEY_PUBLIC'] -api_secret = os.environ['MJ_APIKEY_PRIVATE'] + +api_key = os.environ["MJ_APIKEY_PUBLIC"] +api_secret = os.environ["MJ_APIKEY_PRIVATE"] mailjet = Client(auth=(api_key, api_secret)) -id_ = 'Template_ID' +id_ = "Template_ID" result = mailjet.template.delete(id=id_) print(result.status_code) print(result.json()) ``` +## License + +[MIT](https://choosealicense.com/licenses/mit/) + ## Contribute Mailjet loves developers. You can be part of this project! @@ -397,3 +435,13 @@ Feel free to ask anything, and contribute: - 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 repo](https://github.com/mailjet/api-documentation). + +## Contributors + +- [@diskovod](https://github.com/diskovod) +- [@DanyilNefodov](https://github.com/DanyilNefodov) +- [@skupriienko](https://github.com/skupriienko) + +[api_credential]: https://app.mailjet.com/account/apikeys +[doc]: http://dev.mailjet.com/guides/?python# +[mailjet]: (http://www.mailjet.com/) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index ca59c3f..a160382 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -2,7 +2,10 @@ {% set project = pyproject['project'] %} {% set name = project['name'] %} -{% set version = project['version'] %} +{% set version_match = load_file_regex( + load_file=name.replace('-', '_') + "/_version.py", + regex_pattern='__version__ = "(.+)"') %} +{% set version = version_match[1] %} package: name: {{ name|lower }} @@ -15,6 +18,8 @@ build: number: 0 skip: True # [py<39] script: {{ PYTHON }} -m pip install . --no-deps --no-build-isolation -vv + script_env: + - SETUPTOOLS_SCM_PRETEND_VERSION={{ version }} requirements: host: @@ -25,11 +30,15 @@ requirements: {% endfor %} run: - python - - requests >=2.32.3 + {% for dep in pyproject['project']['dependencies'] %} + - {{ dep.lower() }} + {% endfor %} test: imports: - mailjet_rest + - mailjet_rest.utils + - samples source_files: - tests/test_client.py requires: @@ -38,7 +47,7 @@ test: commands: - pip check # TODO: Add environment variables for tests - #- pytest tests/test_client.py -vv + - pytest tests/test_client.py -vv about: home: {{ project['urls']['Homepage'] }} @@ -49,5 +58,5 @@ about: # description: | # license: {{ project['license']['text'] }} - license_family: {{ project['license']['text'] }} + license_family: {{ project['license']['text'].split('-')[0] }} license_file: LICENSE diff --git a/environment-dev.yaml b/environment-dev.yaml index 9b619c3..cb42993 100644 --- a/environment-dev.yaml +++ b/environment-dev.yaml @@ -6,49 +6,57 @@ dependencies: - python >=3.9 # build & host deps - pip + - setuptools-scm + - # PyPI publishing only + - python-build # runtime deps - requests >=2.32.3 # tests + - conda-forge::pyfakefs + - coverage >=4.5.4 - pytest + - pytest-benchmark - pytest-cov - pytest-xdist - - conda-forge::pyfakefs - - pytest-benchmark - - coverage >=4.5.4 # linters & formatters - - pylint - autopep8 - black + - flake8 - isort - make + - conda-forge::monkeytype + - mypy + - pandas-stubs + - pep8-naming - pycodestyle - pydocstyle - - flake8 - - pep8-naming - - yapf + - pylint + - pyright + - radon - ruff - - mypy - toml - types-requests - - pandas-stubs - - pyright - - radon + - yapf # other - - pre-commit - conda - conda-build - jsonschema - - types-jsonschema + - pre-commit - python-dotenv >=0.19.2 + - types-jsonschema - pip: - - pyupgrade - - bandit - autoflake8 - - refurb + - bandit + - docconvert - monkeytype - pyment >=0.3.3 - pytype - - vulture + - pyupgrade + # refurb doesn't support py39 + #- refurb - scalene >=1.3.16 - snakeviz - typos + - vulture + # PyPI publishing only + - twine diff --git a/mailjet_rest/_version.py b/mailjet_rest/_version.py new file mode 100644 index 0000000..d60e0c1 --- /dev/null +++ b/mailjet_rest/_version.py @@ -0,0 +1 @@ +__version__ = "1.4.0" \ No newline at end of file diff --git a/mailjet_rest/utils/version.py b/mailjet_rest/utils/version.py index 48abae5..814c6a2 100644 --- a/mailjet_rest/utils/version.py +++ b/mailjet_rest/utils/version.py @@ -13,8 +13,33 @@ from __future__ import annotations +import re -VERSION: tuple[int, int, int] = (1, 3, 5) +from mailjet_rest._version import __version__ as package_version + + +def clean_version(version_str: str) -> tuple[int, ...]: + """Clean package version string into 3 item tuple. + + Parameters: + version_str (str): A string of the package version. + + Returns: + tuple: A tuple representing the version of the package. + """ + if not version_str: + return 0, 0, 0 + # Extract just the X.Y.Z part using regex + match = re.match(r"^(\d+\.\d+\.\d+)", version_str) + if match: + version_part = match.group(1) + return tuple(map(int, version_part.split("."))) + + return 0, 0, 0 # type: ignore[unreachable] + + +# VERSION is a tuple of integers (1, 3, 2). +VERSION: tuple[int, ...] = clean_version(package_version) def get_version(version: tuple[int, ...] | None = None) -> str: diff --git a/pyproject.toml b/pyproject.toml index 5c752a3..9289de6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,22 +1,39 @@ [build-system] -requires = ["setuptools>=61.0", "wheel"] +requires = ["setuptools>=61.0", "wheel", "setuptools-scm"] build-backend = "setuptools.build_meta" +[tool.setuptools_scm] +version_scheme = "no-guess-dev" # Don't try to guess next version +local_scheme = "no-local-version" +# Ignore uncommitted changes +git_describe_command = "git describe --tags --match 'v[0-9]*.[0-9]*.[0-9]*'" +write_to = "mailjet_rest/_version.py" +write_to_template = '__version__ = "{version}"' +#fallback_version = "X.Y.ZrcN.postN.devN" # Explicit fallback + +[tool.setuptools] +py-modules = ["mailjet_rest._version"] + [tool.setuptools.packages.find] -include = ["mailjet_rest", "mailjet_rest.*", "tests", "test.py"] +include = ["mailjet_rest", "mailjet_rest.*", "samples", "tests", "test.py"] [tool.setuptools.package-data] mailjet_rest = ["py.typed", "*.pyi"] [project] -name = "mailjet_rest" -version = "1.4.0rc1" +name = "mailjet-rest" +dynamic = ["version"] description = "Mailjet V3 API wrapper" authors = [ { name = "starenka", email = "starenka0@gmail.com" }, { name = "Mailjet", email = "api@mailjet.com" }, ] +maintainers = [ + {name = "Serhii Kupriienko", email = "kupriienko.serhii@gmail.com"} +] license = {text = "MIT"} +# TODO: Enable license-files when setuptools >=77.0.0 will be available +#license-files = ["LICENSE"] readme = "README.md" requires-python = ">=3.9" @@ -34,7 +51,6 @@ classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3", @@ -84,11 +100,17 @@ linting = [ "pyment>=0.3.3", # for generating docstrings "pytype", # a static type checker for any type hints you have put in your code "radon", + "safety", # Checks installed dependencies for known vulnerabilities and licenses. "vulture", # env variables "python-dotenv>=0.19.2", ] +docs = [ + "docconvert", + "pyment>=0.3.3", # for generating docstrings +] + metrics = [ "pystra", # provides functionalities to enable structural reliability analysis "wily>=1.2.0", # a tool for reporting code complexity metrics @@ -114,7 +136,7 @@ other = ["toml"] [tool.black] line-length = 88 -target-version = ["py39", "py310", "py311", "py312"] +target-version = ["py39", "py310", "py311", "py312", "py313"] skip-string-normalization = false skip-magic-trailing-comma = false extend-exclude = ''' @@ -170,6 +192,9 @@ line-length = 88 # Assume Python 3.9. target-version = "py39" +# Enumerate all fixed violations. +show-fixes = true + [tool.ruff.lint] # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or @@ -276,6 +301,11 @@ ignore-overlong-task-comments = true [tool.ruff.lint.pydocstyle] convention = "google" +[tool.pydocstyle] +convention = "google" +match = ".*.py" +match_dir = '^samples/' + [tool.flake8] exclude = ["samples/*"] # TODO: D100 - create docstrings for modules test_client.py and test_version.py @@ -287,6 +317,17 @@ per-file-ignores = [ max-line-length = 88 count = true +[tool.yapf] +based_on_style = "facebook" +SPLIT_BEFORE_BITWISE_OPERATOR = true +SPLIT_BEFORE_ARITHMETIC_OPERATOR = true +SPLIT_BEFORE_LOGICAL_OPERATOR = true +SPLIT_BEFORE_DOT = true + +[tool.yapfignore] +ignore_patterns = [ +] + [tool.mypy] strict = true # Adapted from this StackOverflow post: @@ -334,7 +375,6 @@ exclude = [ ] # Configuring error messages -show-fixes = true show_error_context = false show_column_numbers = false show_error_codes = true @@ -393,3 +433,21 @@ subprocess = [ "subprocess.check_call", "subprocess.check_output" ] + +[tool.coverage.run] +source_pkgs = ["mailjet_rest"] +branch = true +parallel = true +omit = [ + "samples/*", +] + +[tool.coverage.paths] +tests = ["tests"] + +[tool.coverage.report] +exclude_lines = [ + "no cov", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] diff --git a/test.py b/test.py index 7b03ebc..d232aaa 100644 --- a/test.py +++ b/test.py @@ -213,7 +213,7 @@ def test_user_agent(self) -> None: None """ self.client = Client(auth=self.auth, version="v3.1") - self.assertEqual(self.client.config.user_agent, "mailjet-apiv3-python/v1.3.5") + self.assertEqual(self.client.config.user_agent, "mailjet-apiv3-python/v1.4.0") if __name__ == "__main__":