diff --git a/.copier/package.yml b/.copier/package.yml index 7c85071..203c494 100644 --- a/.copier/package.yml +++ b/.copier/package.yml @@ -1,11 +1,10 @@ # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY -_commit: v2024.18 +_commit: v2024.24 _src_path: gh:westerveltco/django-twc-package author_email: josh@joshthomas.dev author_name: Josh Thomas current_version: 0.4.3 django_versions: -- '3.2' - '4.2' - '5.0' docs_domain: westervelt.dev @@ -21,5 +20,6 @@ python_versions: - '3.10' - '3.11' - '3.12' +- '3.13' test_django_main: true versioning_scheme: SemVer diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml deleted file mode 100644 index d3c6246..0000000 --- a/.github/workflows/labels.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: labels - -on: - schedule: - # https://crontab.guru/#30_2_*_*_* - - cron: "30 2 * * *" - workflow_dispatch: - -permissions: - issues: write - -jobs: - labels: - runs-on: ubuntu-latest - - steps: - - uses: EndBug/label-sync@v2 - with: - config-file: https://raw.githubusercontent.com/westerveltco/.github/main/.github/labels.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be64a3b..075ae32 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,53 +4,32 @@ on: release: types: [released] -jobs: - check: - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@v4 - - - name: Check most recent test run on `main` - id: latest-test-result - run: | - echo "result=$(gh run list \ - --branch=main \ - --workflow=test.yml \ - --json headBranch,workflowName,conclusion \ - --jq '.[] | select(.headBranch=="main" and .conclusion=="success") | .conclusion' \ - | head -n 1)" >> $GITHUB_OUTPUT - - - name: OK - if: ${{ (contains(steps.latest-test-result.outputs.result, 'success')) }} - run: exit 0 +permissions: + contents: write - - name: Fail - if: ${{ !contains(steps.latest-test-result.outputs.result, 'success') }} - run: exit 1 +jobs: + test: + uses: ./.github/workflows/test.yml pypi: if: ${{ github.event_name == 'release' }} runs-on: ubuntu-latest - needs: check + needs: test environment: release permissions: contents: read id-token: write steps: - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: westerveltco/setup-ci-action@v0 + - uses: actions/setup-python@v5 with: python-version: 3.12 - extra-python-dependencies: hatch - use-uv: true + + - name: Install dependencies + run: | + python -m pip install -U pip uv + python -m uv pip install --system hatch - name: Build package run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aca2c12..417e9df 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,9 +1,10 @@ name: test on: + pull_request: push: branches: [main] - pull_request: + workflow_call: concurrency: group: test-${{ github.head_ref }} @@ -20,14 +21,15 @@ jobs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: westerveltco/setup-ci-action@v0 + - uses: actions/setup-python@v5 with: python-version: 3.8 - extra-python-dependencies: nox - use-uv: true + + - name: Install dependencies + run: | + python -m pip install -U pip uv + python -m uv pip install --system nox - id: set-matrix run: | @@ -42,14 +44,16 @@ jobs: matrix: ${{ fromJSON(needs.generate-matrix.outputs.matrix) }} steps: - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: westerveltco/setup-ci-action@v0 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - extra-python-dependencies: nox - use-uv: true + allow-prereleases: true + + - name: Install dependencies + run: | + python -m pip install -U pip uv + python -m uv pip install --system nox - name: Run tests run: | @@ -71,14 +75,15 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: westerveltco/setup-ci-action@v0 + - uses: actions/setup-python@v5 with: - python-version: 3.8 - extra-python-dependencies: nox - use-uv: true + python-version: 3.12 + + - name: Install dependencies + run: | + python -m pip install -U pip uv + python -m uv pip install --system nox - name: Run mypy run: | @@ -88,14 +93,15 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: westerveltco/setup-ci-action@v0 + - uses: actions/setup-python@v5 with: python-version: 3.8 - extra-python-dependencies: nox - use-uv: true + + - name: Install dependencies + run: | + python -m pip install -U pip uv + python -m uv pip install --system nox - name: Run coverage run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 378b505..8fd297b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: rev: 1.20.0 hooks: - id: django-upgrade - args: [--target-version, "3.2"] + args: [--target-version, "4.2"] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.5.4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 95e7824..70cd18d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,60 +18,68 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/ ## [Unreleased] +### Added + +- Added support for Python 3.13. + ### Changed -- Now using v2024.18 of `django-twc-package`. +- Now using v2024.24 of `django-twc-package`. + +### Removed + +- Dropped support for Django 3.2. ## [0.4.3] ### Fixed -- Added all documentation pages back to `toctree`. -- Added back missing copyright information from `django-mailer`. Sorry to all the maintainers and contributors of that package! It was a major oversight. -- Added back all missing extras: `hc`, `psycopg`, and `relay`. +- Added all documentation pages back to `toctree`. +- Added back missing copyright information from `django-mailer`. Sorry to all the maintainers and contributors of that package! It was a major oversight. +- Added back all missing extras: `hc`, `psycopg`, and `relay`. ## [0.4.2] ### Fixed -- Added `LICENSE` to `Dockerfile`. +- Added `LICENSE` to `Dockerfile`. ## [0.4.1] ### Fixed -- Added back Docker publishing to `release.yml` GitHub Actions workflow. +- Added back Docker publishing to `release.yml` GitHub Actions workflow. ## [0.4.0] ### Added -- A `_email_relay_version` field to `RelayEmailData` to track the version of the schema used to serialize the data. This should allow for future changes to the schema to be handled more gracefully. +- A `_email_relay_version` field to `RelayEmailData` to track the version of the schema used to serialize the data. This should allow for future changes to the schema to be handled more gracefully. ### Changed -- Now using [`django-twc-package`](https://github.com/westerveltco/django-twc-package) template for repository and package structure. - - This includes using `uv` internally for managing Python dependencies. +- Now using [`django-twc-package`](https://github.com/westerveltco/django-twc-package) template for repository and package structure. + - This includes using `uv` internally for managing Python dependencies. ### Fixed -- Resolved a type mismatch error in from_email_message method when encoding attachments to base64. Added type checking to confirm that the payload extracted from a MIMEBase object is of type bytes before passing it to base64.b64encode. +- Resolved a type mismatch error in from_email_message method when encoding attachments to base64. Added type checking to confirm that the payload extracted from a MIMEBase object is of type bytes before passing it to base64.b64encode. ## [0.3.0] ### Added -- Support for Django 5.0. +- Support for Django 5.0. ### Removed -- Support for Django 4.1. +- Support for Django 4.1. ## [0.2.1] ### Fixed -- Migration 0002 was not being applied to the `default` database, which is the norm when running the relay in Docker. +- Migration 0002 was not being applied to the `default` database, which is the norm when running the relay in Docker. ## [0.2.0] - **YANKED** @@ -81,24 +89,24 @@ This release has been yanked from PyPI due to a bug in the migration that was no ### Added -- A `RelayEmailData` dataclass for representing the `Message.data` field. -- Documentation in the package's deprecation policy about the road to 1.0.0. -- Complete test coverage for all of the public ways of sending emails that Django provides. +- A `RelayEmailData` dataclass for representing the `Message.data` field. +- Documentation in the package's deprecation policy about the road to 1.0.0. +- Complete test coverage for all of the public ways of sending emails that Django provides. ### Changed -- **Breaking**: The internal JSON schema for the `Message.data` field has been updated to bring it more in line with all of the possible fields provided by Django's `EmailMessage` and `EmailMultiAlternatives`. This change involves a migration to update the `Message.data` field to the new schema. This is a one-way update and cannot be rolled back. Please take care when updating to this version and ensure that all projects using `django-email-relay` are updated at the same time. See the [updating](https://django-email-relay.westervelt.dev/en/latest/updating.html) documentation for more information. -- The internal `AppSettings` dataclass is now a `frozen=True` dataclass. +- **Breaking**: The internal JSON schema for the `Message.data` field has been updated to bring it more in line with all of the possible fields provided by Django's `EmailMessage` and `EmailMultiAlternatives`. This change involves a migration to update the `Message.data` field to the new schema. This is a one-way update and cannot be rolled back. Please take care when updating to this version and ensure that all projects using `django-email-relay` are updated at the same time. See the [updating](https://django-email-relay.westervelt.dev/en/latest/updating.html) documentation for more information. +- The internal `AppSettings` dataclass is now a `frozen=True` dataclass. ## [0.1.1] ### Added -- Moved a handful of common `Message` queries and actions from the `runrelay` management command to methods on the `MessageManager` class. +- Moved a handful of common `Message` queries and actions from the `runrelay` management command to methods on the `MessageManager` class. ### Fixed -- The relay service would crash if requests raised an `Exception` during the healthcheck ping. Now it will log the exception and continue processing the queue. +- The relay service would crash if requests raised an `Exception` during the healthcheck ping. Now it will log the exception and continue processing the queue. ## [0.1.0] @@ -106,25 +114,25 @@ Initial release! ### Added -- An email backend that stores emails in a database ala a Message model rather than sending them via SMTP or other means. - - Designed to work seamlessly with Django's built-in ways of sending emails. -- A database backend that routes writes to the Message model to a separate database. -- A Message model that stores the contents of an email. - - The Message model stores the contents of the email as a JSONField. - - Attachments are stored in the database, under the 'attachments' key in the JSONField. - - Should be able to handle anything that Django can by default. -- A relay service intended to be run separately, either as a standalone Docker image or as a management command within a Django project. - - A Docker image is provided for the relay service. Currently only PostgreSQL is supported as a database backend. - - A management command is provided for the relay service. Any database backend supported by Django should work (minus SQLite which doesn't make sense for a relay service). - - The relay service can be configured with a healthcheck url to ensure it is running. -- Initial documentation. -- Initial tests. -- Initial CI/CD (GitHub Actions). +- An email backend that stores emails in a database ala a Message model rather than sending them via SMTP or other means. + - Designed to work seamlessly with Django's built-in ways of sending emails. +- A database backend that routes writes to the Message model to a separate database. +- A Message model that stores the contents of an email. + - The Message model stores the contents of the email as a JSONField. + - Attachments are stored in the database, under the 'attachments' key in the JSONField. + - Should be able to handle anything that Django can by default. +- A relay service intended to be run separately, either as a standalone Docker image or as a management command within a Django project. + - A Docker image is provided for the relay service. Currently only PostgreSQL is supported as a database backend. + - A management command is provided for the relay service. Any database backend supported by Django should work (minus SQLite which doesn't make sense for a relay service). + - The relay service can be configured with a healthcheck url to ensure it is running. +- Initial documentation. +- Initial tests. +- Initial CI/CD (GitHub Actions). ### New Contributors -- Josh Thomas (maintainer) -- Jeff Triplett [@jefftriplett](https://github.com/jefftriplett) +- Josh Thomas (maintainer) +- Jeff Triplett [@jefftriplett](https://github.com/jefftriplett) ### Thanks ❤️ diff --git a/README.md b/README.md index 57a1231..e6c6c76 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ [![PyPI](https://img.shields.io/pypi/v/django-email-relay)](https://pypi.org/project/django-email-relay/) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-email-relay) -![Django Version](https://img.shields.io/badge/django-3.2%20%7C%204.2%20%7C%205.0-%2344B78B?labelColor=%23092E20) +![Django Version](https://img.shields.io/badge/django-4.2%20%7C%205.0-%2344B78B?labelColor=%23092E20) - + `django-email-relay` enables Django projects without direct access to a preferred SMTP server to use that server for email dispatch. @@ -19,8 +19,8 @@ It consists of two parts: ## Requirements -- Python 3.8, 3.9, 3.10, 3.11, 3.12 -- Django 3.2, 4.2, 5.0 +- Python 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 +- Django 4.2, 5.0 ## Getting Started diff --git a/noxfile.py b/noxfile.py index 5dfcbb8..4685e74 100644 --- a/noxfile.py +++ b/noxfile.py @@ -13,17 +13,17 @@ PY310 = "3.10" PY311 = "3.11" PY312 = "3.12" -PY_VERSIONS = [PY38, PY39, PY310, PY311, PY312] +PY313 = "3.13" +PY_VERSIONS = [PY38, PY39, PY310, PY311, PY312, PY313] PY_DEFAULT = PY_VERSIONS[0] PY_LATEST = PY_VERSIONS[-1] -DJ32 = "3.2" DJ42 = "4.2" DJ50 = "5.0" DJMAIN = "main" DJMAIN_MIN_PY = PY310 -DJ_VERSIONS = [DJ32, DJ42, DJ50, DJMAIN] -DJ_LTS = [DJ32, DJ42] +DJ_VERSIONS = [DJ42, DJ50, DJMAIN] +DJ_LTS = [DJ42] DJ_DEFAULT = DJ_LTS[0] DJ_LATEST = DJ_VERSIONS[-2] @@ -40,10 +40,6 @@ def should_skip(python: str, django: str) -> bool: # Django main requires Python 3.10+ return True - if django == DJ32 and version(python) >= version(PY312): - # Django 3.2 requires Python < 3.12 - return True - if django == DJ50 and version(python) < version(PY310): # Django 5.0 requires Python 3.10+ return True diff --git a/pyproject.toml b/pyproject.toml index 009309e..33707dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,6 @@ authors = [{name = "Josh Thomas", email = "josh@joshthomas.dev"}] classifiers = [ "Development Status :: 4 - Beta", "Framework :: Django", - "Framework :: Django :: 3.2", "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", "License :: OSI Approved :: MIT License", @@ -20,9 +19,12 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython" ] -dependencies = ["django>=3.2"] +dependencies = [ + "django>=4.2" +] description = "Centralize and relay email from multiple distributed Django projects to an internal SMTP server via a database queue." dynamic = ["version"] keywords = [] @@ -53,6 +55,7 @@ dev = [ "pytest-reverse", "pytest-xdist", "responses", + "ruff", "types-requests" ] docs = [ @@ -110,7 +113,12 @@ django_settings_module = "tests.settings" strict_settings = false [tool.djlint] +blank_line_after_tag = "endblock,endpartialdef,extends,load" +blank_line_before_tag = "block,partialdef" +custom_blocks = "partialdef" +ignore = "H031" # Don't require `meta` tag keywords indent = 2 +profile = "django" [tool.hatch.build] exclude = [".*", "Justfile"] @@ -123,7 +131,13 @@ path = "src/email_relay/__init__.py" [tool.mypy] check_untyped_defs = true -exclude = "docs/.*\\.py$" +exclude = [ + "docs", + "tests", + "migrations", + "venv", + ".venv" +] mypy_path = "src/" no_implicit_optional = true plugins = ["mypy_django_plugin.main"] @@ -134,7 +148,11 @@ warn_unused_ignores = true [[tool.mypy.overrides]] ignore_errors = true ignore_missing_imports = true -module = ["email_relay.*.migrations.*", "tests.*"] +module = [ + "*.migrations.*", + "docs.*", + "tests.*" +] [tool.mypy_django_plugin] ignore_missing_model_attributes = true diff --git a/src/email_relay/_typing.py b/src/email_relay/_typing.py new file mode 100644 index 0000000..73adda6 --- /dev/null +++ b/src/email_relay/_typing.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +import sys + +if sys.version_info >= (3, 12): + from typing import override as typing_override +else: # pragma: no cover + from typing_extensions import ( + override as typing_override, # pyright: ignore[reportUnreachable] + ) + +override = typing_override