mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-19 19:31:06 -05:00
Modernize packaging
Switch to pyproject.toml packaging, using hatchling. - Replace all uses of setup.py with updated equivalent - BREAKING: Change extra name `amazon_ses` to `amazon-ses`, to comply with Python packaging name normalization - Use hatch custom build hook to freeze version number in readme (previously custom setup.py code) - Move separate requirements for dev, docs, tests into their own requirements.txt files - Fix AnymailImproperlyInstalled to correctly refer to package extra name - Update testing documentation - Update docs readme rendering to match PyPI (and avoid setup.py) - In tox tests, use isolated builds and update pip - Remove AUTHORS.txt (it just referred to GitHub)
This commit is contained in:
18
.github/workflows/release.yml
vendored
18
.github/workflows/release.yml
vendored
@@ -24,19 +24,24 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Get code
|
- name: Get code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
|
|
||||||
|
- name: Install build requirements
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade build hatch twine
|
||||||
|
|
||||||
- name: Get version
|
- name: Get version
|
||||||
# (This will end the workflow if git and source versions don't match.)
|
# (This will end the workflow if git and source versions don't match.)
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
VERSION="$(python setup.py --version)"
|
VERSION="$(python -m hatch version)"
|
||||||
TAG="v$VERSION"
|
TAG="v$VERSION"
|
||||||
GIT_TAG="$(git tag -l --points-at "$GITHUB_REF" 'v*')"
|
GIT_TAG="$(git tag -l --points-at "$GITHUB_REF" 'v*')"
|
||||||
if [ "$GIT_TAG" != "$TAG" ]; then
|
if [ "x$GIT_TAG" != "x$TAG" ]; then
|
||||||
echo "::error ::package version '$TAG' does not match git tag '$GIT_TAG'"
|
echo "::error ::package version '$TAG' does not match git tag '$GIT_TAG'"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -44,21 +49,18 @@ jobs:
|
|||||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||||
echo "anchor=${TAG//[^[:alnum:]]/-}" >> $GITHUB_OUTPUT
|
echo "anchor=${TAG//[^[:alnum:]]/-}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Install build requirements
|
|
||||||
run: |
|
|
||||||
pip install twine wheel
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
rm -rf build dist django_anymail.egg-info
|
rm -rf build dist django_anymail.egg-info
|
||||||
python setup.py sdist bdist_wheel
|
python -m build
|
||||||
twine check dist/*
|
python -m twine check dist/*
|
||||||
|
|
||||||
- name: Publish to PyPI
|
- name: Publish to PyPI
|
||||||
env:
|
env:
|
||||||
TWINE_USERNAME: __token__
|
TWINE_USERNAME: __token__
|
||||||
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
twine upload dist/*
|
python -m twine upload dist/*
|
||||||
|
|
||||||
- name: Release to GitHub
|
- name: Release to GitHub
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -38,6 +38,20 @@ tests regularly. That requires the ESP have a free tier (testing is
|
|||||||
extremely low volume), a sandbox API, or that they offer developer
|
extremely low volume), a sandbox API, or that they offer developer
|
||||||
accounts for open source projects like Anymail.
|
accounts for open source projects like Anymail.
|
||||||
|
|
||||||
|
## Boilerplate
|
||||||
|
|
||||||
|
You should add entries for your ESP in:
|
||||||
|
|
||||||
|
- pyproject.toml:
|
||||||
|
- in the `[project]` metadata section under `description` and `keywords`
|
||||||
|
- in the `[project.optional-dependencies]` section
|
||||||
|
- integration-test.yml in the test matrix
|
||||||
|
- tox.ini in the `[testenv]` section under `setenv`
|
||||||
|
- if your ESP requires any extra dependencies, also update the tox.ini
|
||||||
|
`[testenv] extras` and the "partial installation" at the bottom of
|
||||||
|
`[tox] envlist`
|
||||||
|
- README.rst in the list of ESPs
|
||||||
|
|
||||||
## EmailBackend and payload
|
## EmailBackend and payload
|
||||||
|
|
||||||
Anymail abstracts a lot of common functionality into its base classes;
|
Anymail abstracts a lot of common functionality into its base classes;
|
||||||
@@ -99,9 +113,6 @@ Need to parse JSON in the API response? Use `self.deserialize_json_response()`
|
|||||||
|
|
||||||
Good starting points: Test backend; SparkPost
|
Good starting points: Test backend; SparkPost
|
||||||
|
|
||||||
Don't forget add an `'extras_require'` entry for your ESP in setup.py.
|
|
||||||
Also update `'tests_require'`.
|
|
||||||
|
|
||||||
If the client lib supports the notion of a reusable API "connection"
|
If the client lib supports the notion of a reusable API "connection"
|
||||||
(or session), you should override `open()` and `close()` to provide
|
(or session), you should override `open()` and `close()` to provide
|
||||||
API state caching. See the notes in the base implementation.
|
API state caching. See the notes in the base implementation.
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
Anymail
|
|
||||||
=======
|
|
||||||
|
|
||||||
Please see https://github.com/anymail/django-anymail/graphs/contributors
|
|
||||||
for the complete list of Anymail contributors.
|
|
||||||
@@ -33,9 +33,25 @@ vNext
|
|||||||
Breaking changes
|
Breaking changes
|
||||||
~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
* **Amazon SES:** The "extra name" for installation must now be spelled with
|
||||||
|
a hyphen rather than an underscore: ``django-anymail[amazon-ses]``.
|
||||||
|
Be sure to update any dependencies specification (pip install, requirements.txt,
|
||||||
|
etc.) that had been using ``[amazon_ses]``.
|
||||||
|
|
||||||
* Require Python 3.7 or later.
|
* Require Python 3.7 or later.
|
||||||
|
|
||||||
* Require urllib3 1.25 or later (released 2019-04-29).
|
* Require urllib3 1.25 or later (released 2019-04-29).
|
||||||
|
|
||||||
|
Other
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
* Modernize packaging. (Change from setup.py and setuptools
|
||||||
|
to pyproject.toml and hatchling.) Other than the ``amazon-ses``
|
||||||
|
naming normalization noted above, the new packaging should have
|
||||||
|
no impact. If you have trouble installing django-anymail v10 where
|
||||||
|
v9 worked, please report an issue including the exact install
|
||||||
|
command and pip version you are using.
|
||||||
|
|
||||||
|
|
||||||
v9.2
|
v9.2
|
||||||
-----
|
-----
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
include README.rst AUTHORS.txt LICENSE
|
|
||||||
recursive-include anymail *.py
|
|
||||||
@@ -5,9 +5,8 @@ Anymail: Django email integration for transactional ESPs
|
|||||||
* Github: project page, exactly as it appears here
|
* Github: project page, exactly as it appears here
|
||||||
* Docs: shared-intro section gets included in docs/index.rst
|
* Docs: shared-intro section gets included in docs/index.rst
|
||||||
quickstart section gets included in docs/quickstart.rst
|
quickstart section gets included in docs/quickstart.rst
|
||||||
* PyPI: project page (via setup.py long_description),
|
* PyPI: project page (via pyproject.toml readme; see also
|
||||||
with several edits to freeze it to the specific PyPI release
|
hatch_build.py which edits in the release version number)
|
||||||
(see long_description_from_readme in setup.py)
|
|
||||||
You can use docutils 1.0 markup, but *not* any Sphinx additions.
|
You can use docutils 1.0 markup, but *not* any Sphinx additions.
|
||||||
GitHub rst supports code-block, but *no other* block directives.
|
GitHub rst supports code-block, but *no other* block directives.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
# Expose package version at root of package
|
from ._version import VERSION, __version__
|
||||||
from django import VERSION as DJANGO_VERSION
|
|
||||||
|
|
||||||
from ._version import VERSION, __version__ # NOQA: F401
|
__all__ = [
|
||||||
|
"VERSION",
|
||||||
|
"__version__",
|
||||||
|
]
|
||||||
|
|
||||||
if DJANGO_VERSION < (3, 2, 0):
|
try:
|
||||||
default_app_config = "anymail.apps.AnymailBaseConfig"
|
import django
|
||||||
|
except ImportError:
|
||||||
|
# (don't require django just to get package version)
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if django.VERSION < (3, 2, 0):
|
||||||
|
# (No longer required -- and causes deprecation warning -- in Django 3.2+)
|
||||||
|
default_app_config = "anymail.apps.AnymailBaseConfig"
|
||||||
|
__all__.append("default_app_config")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
VERSION = (9, 2)
|
# Don't import this file directly (unless you are a build system).
|
||||||
|
# Instead, load version info from the package root.
|
||||||
|
|
||||||
#: major.minor.patch or major.minor.devN
|
#: major.minor.patch or major.minor.devN
|
||||||
__version__ = ".".join([str(x) for x in VERSION])
|
__version__ = "10.0.dev0"
|
||||||
|
|
||||||
#: Sphinx's X.Y "version"
|
VERSION = __version__.split(",")
|
||||||
__minor_version__ = ".".join([str(x) for x in VERSION[:2]])
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ try:
|
|||||||
from botocore.exceptions import BotoCoreError, ClientError, ConnectionError
|
from botocore.exceptions import BotoCoreError, ClientError, ConnectionError
|
||||||
except ImportError as err:
|
except ImportError as err:
|
||||||
raise AnymailImproperlyInstalled(
|
raise AnymailImproperlyInstalled(
|
||||||
missing_package="boto3", backend="amazon_ses"
|
missing_package="boto3", install_extra="amazon-ses"
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ try:
|
|||||||
from botocore.exceptions import BotoCoreError, ClientError, ConnectionError
|
from botocore.exceptions import BotoCoreError, ClientError, ConnectionError
|
||||||
except ImportError as err:
|
except ImportError as err:
|
||||||
raise AnymailImproperlyInstalled(
|
raise AnymailImproperlyInstalled(
|
||||||
missing_package="boto3", backend="amazon_sesv2"
|
missing_package="boto3", install_extra="amazon-ses"
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -171,11 +171,13 @@ class AnymailConfigurationError(ImproperlyConfigured):
|
|||||||
class AnymailImproperlyInstalled(AnymailConfigurationError, ImportError):
|
class AnymailImproperlyInstalled(AnymailConfigurationError, ImportError):
|
||||||
"""Exception for Anymail missing package dependencies"""
|
"""Exception for Anymail missing package dependencies"""
|
||||||
|
|
||||||
def __init__(self, missing_package, backend="<backend>"):
|
def __init__(self, missing_package, install_extra="<esp>"):
|
||||||
|
# install_extra must be the package "optional extras name" for the ESP
|
||||||
|
# (not the backend's esp_name)
|
||||||
message = (
|
message = (
|
||||||
"The %s package is required to use this ESP, but isn't installed.\n"
|
"The %s package is required to use this ESP, but isn't installed.\n"
|
||||||
"(Be sure to use `pip install django-anymail[%s]` "
|
'(Be sure to use `pip install "django-anymail[%s]"` '
|
||||||
"with your desired ESPs.)" % (missing_package, backend)
|
"with your desired ESP name(s).)" % (missing_package, install_extra)
|
||||||
)
|
)
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ except ImportError:
|
|||||||
# This module gets imported by anymail.urls, so don't complain about boto3 missing
|
# This module gets imported by anymail.urls, so don't complain about boto3 missing
|
||||||
# unless one of the Amazon SES webhook views is actually used and needs it
|
# unless one of the Amazon SES webhook views is actually used and needs it
|
||||||
boto3 = _LazyError(
|
boto3 = _LazyError(
|
||||||
AnymailImproperlyInstalled(missing_package="boto3", backend="amazon_ses")
|
AnymailImproperlyInstalled(missing_package="boto3", install_extra="amazon-ses")
|
||||||
)
|
)
|
||||||
ClientError = object
|
ClientError = object
|
||||||
_get_anymail_boto3_params = _LazyError(
|
_get_anymail_boto3_params = _LazyError(
|
||||||
AnymailImproperlyInstalled(missing_package="boto3", backend="amazon_ses")
|
AnymailImproperlyInstalled(missing_package="boto3", install_extra="amazon-ses")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ except ImportError:
|
|||||||
# This module gets imported by anymail.urls, so don't complain about cryptography
|
# This module gets imported by anymail.urls, so don't complain about cryptography
|
||||||
# missing unless one of the Postal webhook views is actually used and needs it
|
# missing unless one of the Postal webhook views is actually used and needs it
|
||||||
error = _LazyError(
|
error = _LazyError(
|
||||||
AnymailImproperlyInstalled(missing_package="cryptography", backend="postal")
|
AnymailImproperlyInstalled(
|
||||||
|
missing_package="cryptography", install_extra="postal"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
serialization = error
|
serialization = error
|
||||||
hashes = error
|
hashes = error
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
# docutils (rst2html) config for generating static HTML that approximates
|
|
||||||
# PyPI package description rendering (as of 3/2018).
|
|
||||||
#
|
|
||||||
# Usage (in package root dir):
|
|
||||||
# python setup.py --long-description | rst2html.py --config=docs/_readme/docutils.cfg > ${OUTDIR}/readme.html
|
|
||||||
#
|
|
||||||
# Requires docutils and pygments (both are installed with Sphinx)
|
|
||||||
|
|
||||||
[general]
|
|
||||||
# Duplicate docutils config used by PyPA readme_renderer.
|
|
||||||
# https://github.com/pypa/readme_renderer/blob/master/readme_renderer/rst.py
|
|
||||||
cloak_email_addresses = True
|
|
||||||
doctitle_xform = True
|
|
||||||
sectsubtitle_xform = True
|
|
||||||
initial_header_level = 2
|
|
||||||
file_insertion_enabled = False
|
|
||||||
math_output = MathJax
|
|
||||||
raw_enabled = False
|
|
||||||
smart_quotes = True
|
|
||||||
strip_comments = True
|
|
||||||
syntax_highlight = short
|
|
||||||
|
|
||||||
# Halt rendering and throw an exception if there was any errors or warnings from docutils.
|
|
||||||
halt_level = 2
|
|
||||||
# DON'T Disable all system messages from being reported.
|
|
||||||
# (We're not running inside readme_renderer, so *do* want to see warnings and errors.)
|
|
||||||
# report_level = 5
|
|
||||||
|
|
||||||
# Approximate PyPI's layout and styles:
|
|
||||||
template = docs/_readme/template.txt
|
|
||||||
109
docs/_readme/render.py
Normal file
109
docs/_readme/render.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# Render a README file (roughly) as it would appear on PyPI
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from importlib.metadata import PackageNotFoundError, metadata
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
import readme_renderer.rst
|
||||||
|
from docutils.core import publish_string
|
||||||
|
from docutils.utils import SystemMessage
|
||||||
|
|
||||||
|
# Docutils template.txt in our directory:
|
||||||
|
DEFAULT_TEMPLATE_FILE = Path(__file__).with_name("template.txt").absolute()
|
||||||
|
|
||||||
|
|
||||||
|
def get_package_readme(package: str) -> str:
|
||||||
|
# Note: "description" was added to metadata in Python 3.10
|
||||||
|
return metadata(package)["description"]
|
||||||
|
|
||||||
|
|
||||||
|
class ReadMeHTMLWriter(readme_renderer.rst.Writer):
|
||||||
|
translator_class = readme_renderer.rst.ReadMeHTMLTranslator
|
||||||
|
|
||||||
|
def interpolation_dict(self) -> Dict[str, str]:
|
||||||
|
result = super().interpolation_dict()
|
||||||
|
# clean the same parts as readme_renderer.rst.render:
|
||||||
|
clean = readme_renderer.rst.clean
|
||||||
|
result["docinfo"] = clean(result["docinfo"])
|
||||||
|
result["body"] = result["fragment"] = clean(result["fragment"])
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def render(source_text: str, warning_stream=sys.stderr) -> Optional[str]:
|
||||||
|
# Adapted from readme_renderer.rst.render
|
||||||
|
settings = readme_renderer.rst.SETTINGS.copy()
|
||||||
|
settings.update(
|
||||||
|
{
|
||||||
|
"warning_stream": warning_stream,
|
||||||
|
"template": DEFAULT_TEMPLATE_FILE,
|
||||||
|
# Input and output are text str (we handle decoding/encoding):
|
||||||
|
"input_encoding": "unicode",
|
||||||
|
"output_encoding": "unicode",
|
||||||
|
# Exit with error on docutils warning or above.
|
||||||
|
# (There's discussion of having readme_renderer ignore warnings;
|
||||||
|
# this ensures they'll be treated as errors here.)
|
||||||
|
"halt_level": 2, # (docutils.utils.Reporter.WARNING_LEVEL)
|
||||||
|
# Report all docutils warnings or above.
|
||||||
|
# (The readme_renderer default suppresses this output.)
|
||||||
|
"report_level": 2, # (docutils.utils.Reporter.WARNING_LEVEL)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
writer = ReadMeHTMLWriter()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return publish_string(
|
||||||
|
source_text,
|
||||||
|
writer=writer,
|
||||||
|
settings_overrides=settings,
|
||||||
|
)
|
||||||
|
except SystemMessage:
|
||||||
|
warning_stream.write("Error rendering readme source.\n")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Render readme file as it would appear on PyPI"
|
||||||
|
)
|
||||||
|
input_group = parser.add_mutually_exclusive_group(required=True)
|
||||||
|
input_group.add_argument(
|
||||||
|
"-p", "--package", help="Source readme from package's metadata"
|
||||||
|
)
|
||||||
|
input_group.add_argument(
|
||||||
|
"-i",
|
||||||
|
"--input",
|
||||||
|
help="Source readme.rst file ('-' for stdin)",
|
||||||
|
type=argparse.FileType("r"),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-o",
|
||||||
|
"--output",
|
||||||
|
help="Output file (default: stdout)",
|
||||||
|
type=argparse.FileType("w"),
|
||||||
|
default="-",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
if args.package:
|
||||||
|
try:
|
||||||
|
source_text = get_package_readme(args.package)
|
||||||
|
except PackageNotFoundError:
|
||||||
|
print(f"Package not installed: {args.package!r}", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
if source_text is None:
|
||||||
|
print(f"No metadata readme for {args.package!r}", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
else:
|
||||||
|
source_text = args.input.read()
|
||||||
|
rendered = render(source_text)
|
||||||
|
if rendered is None:
|
||||||
|
sys.exit(2)
|
||||||
|
args.output.write(rendered)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
%(head_prefix)s
|
%(head_prefix)s
|
||||||
<!--
|
<!--
|
||||||
This approximates PyPI.org project page styling as of 8/2020,
|
This approximates PyPI.org project page styling as of 5/2023,
|
||||||
and loads their compiled CSS that was in use at that time.
|
and loads their compiled CSS that was in use at that time.
|
||||||
|
|
||||||
(Styling seems to change more often than basic page structure,
|
(Styling seems to change more often than basic page structure,
|
||||||
so to update, it may be sufficient to copy in the current
|
so to update, it may be sufficient to copy in the current
|
||||||
<link rel="stylesheet" ...> tags from any live package page.)
|
<link rel="stylesheet" ...> tags from any live package page.
|
||||||
|
Be sure to convert or escape any percent chars in copied urls,
|
||||||
|
to avoid "not enough arguments for format string" errors.)
|
||||||
|
|
||||||
This extends the docutils base template found at
|
This extends the docutils base template found at
|
||||||
${SITE_PACKAGES}/docutils/writers/html5_polyglot/template.txt
|
${SITE_PACKAGES}/docutils/writers/html5_polyglot/template.txt
|
||||||
@@ -15,13 +17,13 @@
|
|||||||
%(head)s
|
%(head)s
|
||||||
<!-- template (stylesheet) omitted -->
|
<!-- template (stylesheet) omitted -->
|
||||||
|
|
||||||
<link rel="stylesheet" href="/static/css/warehouse-ltr.f2d4f304.css">
|
<link rel="stylesheet" href="/static/css/warehouse-ltr.a42ccb04.css">
|
||||||
<link rel="stylesheet" href="/static/css/fontawesome.6002a161.css">
|
<link rel="stylesheet" href="/static/css/fontawesome.d37999f3.css">
|
||||||
<link rel="stylesheet" href="/static/css/regular.98fbf39a.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/solid.c3b5f0b5.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/brands.2c303be1.css">
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400italic,600,600italic,700,700italic|Source+Code+Pro:500">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400italic,600,600italic,700,700italic|Source+Code+Pro:500">
|
||||||
<link rel="icon" href="/static/images/favicon.6a76275d.ico" type="image/x-icon">
|
<noscript>
|
||||||
|
<link rel="stylesheet" href="/static/css/noscript.0673c9ea.css">
|
||||||
|
</noscript>
|
||||||
|
<link rel="icon" href="/static/images/favicon.35549fe8.ico" type="image/x-icon">
|
||||||
|
|
||||||
%(body_prefix)s
|
%(body_prefix)s
|
||||||
|
|
||||||
|
|||||||
17
docs/conf.py
17
docs/conf.py
@@ -13,6 +13,8 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from anymail import VERSION as PACKAGE_VERSION
|
||||||
|
|
||||||
ON_READTHEDOCS = os.environ.get("READTHEDOCS", None) == "True"
|
ON_READTHEDOCS = os.environ.get("READTHEDOCS", None) == "True"
|
||||||
DOCS_PATH = Path(__file__).parent
|
DOCS_PATH = Path(__file__).parent
|
||||||
PROJECT_ROOT_PATH = DOCS_PATH.parent
|
PROJECT_ROOT_PATH = DOCS_PATH.parent
|
||||||
@@ -22,15 +24,6 @@ PROJECT_ROOT_PATH = DOCS_PATH.parent
|
|||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
sys.path.insert(0, PROJECT_ROOT_PATH.resolve())
|
sys.path.insert(0, PROJECT_ROOT_PATH.resolve())
|
||||||
|
|
||||||
# define __version__ and __minor_version__ from ../anymail/_version.py,
|
|
||||||
# but without importing from anymail (which would make docs dependent on Django, etc.)
|
|
||||||
__version__ = "UNSET"
|
|
||||||
__minor_version__ = "UNSET"
|
|
||||||
version_path = PROJECT_ROOT_PATH / "anymail/_version.py"
|
|
||||||
code = compile(version_path.read_text(), version_path, "exec")
|
|
||||||
exec(code)
|
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration -----------------------------------------------------
|
# -- General configuration -----------------------------------------------------
|
||||||
|
|
||||||
# If your documentation needs a minimal Sphinx version, state it here.
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
@@ -61,10 +54,10 @@ copyright = "Anymail contributors"
|
|||||||
# |version| and |release|, also used in various other places throughout the
|
# |version| and |release|, also used in various other places throughout the
|
||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
|
||||||
version = __minor_version__
|
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = __version__
|
release = ".".join(PACKAGE_VERSION)
|
||||||
|
# The short X.Y version.
|
||||||
|
version = ".".join(PACKAGE_VERSION[:2])
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ The `Anymail source code`_ is on GitHub.
|
|||||||
Contributors
|
Contributors
|
||||||
------------
|
------------
|
||||||
|
|
||||||
See `AUTHORS.txt`_ for a list of some of the people who have helped
|
See the `contributor chart`_ for a list of some of the people who have helped
|
||||||
improve Anymail.
|
improve Anymail.
|
||||||
|
|
||||||
Anymail evolved from the `Djrill`_ project. Special thanks to the
|
Anymail evolved from the `Djrill`_ project. Special thanks to the
|
||||||
folks from `brack3t`_ who developed the original version of Djrill.
|
folks from `brack3t`_ who developed the original version of Djrill.
|
||||||
|
|
||||||
.. _AUTHORS.txt: https://github.com/anymail/django-anymail/blob/main/AUTHORS.txt
|
.. _contributor chart: https://github.com/anymail/django-anymail/graphs/contributors
|
||||||
.. _brack3t: http://brack3t.com/
|
.. _brack3t: http://brack3t.com/
|
||||||
.. _Djrill: https://github.com/brack3t/Djrill
|
.. _Djrill: https://github.com/brack3t/Djrill
|
||||||
|
|
||||||
@@ -72,48 +72,42 @@ Anymail is `tested via GitHub Actions`_ against several combinations of Django
|
|||||||
and Python versions. Tests are run at least once a week, to check whether ESP APIs
|
and Python versions. Tests are run at least once a week, to check whether ESP APIs
|
||||||
and other dependencies have changed out from under Anymail.
|
and other dependencies have changed out from under Anymail.
|
||||||
|
|
||||||
For local development, the recommended test command is
|
To run the tests locally, use :pypi:`tox`:
|
||||||
:shell:`tox -e django31-py38-all,django20-py35-all,lint`, which tests a representative
|
|
||||||
combination of Python and Django versions. It also runs :pypi:`flake8` and other
|
|
||||||
code-style checkers. Some other test options are covered below, but using this
|
|
||||||
tox command catches most problems, and is a good pre-pull-request check.
|
|
||||||
|
|
||||||
Most of the included tests verify that Anymail constructs the expected ESP API
|
|
||||||
calls, without actually calling the ESP's API or sending any email. So these tests
|
|
||||||
don't require API keys, but they *do* require :pypi:`mock` and all ESP-specific
|
|
||||||
package requirements.
|
|
||||||
|
|
||||||
To run the tests, you can:
|
|
||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
|
|
||||||
$ python setup.py test # (also installs test dependencies if needed)
|
## install tox and other development requirements:
|
||||||
|
$ python -m pip install -r requirements-dev.txt
|
||||||
|
|
||||||
Or:
|
## test a representative combination of Python and Django versions:
|
||||||
|
$ tox -e lint,django42-py311-all,django30-py37-all,docs
|
||||||
|
|
||||||
|
## you can also run just some test cases, e.g.:
|
||||||
|
$ tox -e django42-py311-all tests.test_mailgun_backend tests.test_utils
|
||||||
|
|
||||||
|
## to test more Python/Django versions:
|
||||||
|
$ tox --parallel auto # ALL 20+ envs! (in parallel if possible)
|
||||||
|
|
||||||
|
(If your system doesn't come with the necessary Python versions, `pyenv`_ is helpful
|
||||||
|
to install and manage them. Or use the :shell:`--skip-missing-interpreters` tox option.)
|
||||||
|
|
||||||
|
If you don't want to use tox (or have trouble getting it working), you can run
|
||||||
|
the tests in your current Python environment:
|
||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
|
|
||||||
$ pip install mock boto3 # install test dependencies
|
## install the testing requirements (if any):
|
||||||
|
$ python -m pip install -r tests/requirements.txt
|
||||||
|
|
||||||
|
## run the tests:
|
||||||
$ python runtests.py
|
$ python runtests.py
|
||||||
|
|
||||||
## this command can also run just a few test cases, e.g.:
|
## this command can also run just a few test cases, e.g.:
|
||||||
$ python runtests.py tests.test_mailgun_backend tests.test_mailgun_webhooks
|
$ python runtests.py tests.test_mailgun_backend tests.test_mailgun_webhooks
|
||||||
|
|
||||||
Or to test against multiple versions of Python and Django all at once, use :pypi:`tox`.
|
Most of the included tests verify that Anymail constructs the expected ESP API
|
||||||
You'll need some version of Python 3 available. (If your system doesn't come
|
calls, without actually calling the ESP's API or sending any email. (So these
|
||||||
with that, `pyenv`_ is a helpful way to install and manage multiple Python versions.)
|
tests don't require any API keys.)
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ pip install tox # (if you haven't already)
|
|
||||||
$ tox -e django31-py38-all,django20-py35-all,lint # test recommended environments
|
|
||||||
|
|
||||||
## you can also run just some test cases, e.g.:
|
|
||||||
$ tox -e django31-py38-all,django20-py35-all tests.test_mailgun_backend tests.test_utils
|
|
||||||
|
|
||||||
## to test more Python/Django versions:
|
|
||||||
$ tox --parallel auto # ALL 20+ envs! (in parallel if possible)
|
|
||||||
$ tox --skip-missing-interpreters # if some Python versions aren't installed
|
|
||||||
|
|
||||||
In addition to the mocked tests, Anymail has integration tests which *do* call live ESP APIs.
|
In addition to the mocked tests, Anymail has integration tests which *do* call live ESP APIs.
|
||||||
These tests are normally skipped; to run them, set environment variables with the necessary
|
These tests are normally skipped; to run them, set environment variables with the necessary
|
||||||
@@ -123,20 +117,19 @@ API keys or other settings. For example:
|
|||||||
|
|
||||||
$ export ANYMAIL_TEST_MAILGUN_API_KEY='your-Mailgun-API-key'
|
$ export ANYMAIL_TEST_MAILGUN_API_KEY='your-Mailgun-API-key'
|
||||||
$ export ANYMAIL_TEST_MAILGUN_DOMAIN='mail.example.com' # sending domain for that API key
|
$ export ANYMAIL_TEST_MAILGUN_DOMAIN='mail.example.com' # sending domain for that API key
|
||||||
$ tox -e django31-py38-all tests.test_mailgun_integration
|
$ tox -e django42-py311-all tests.test_mailgun_integration
|
||||||
|
|
||||||
Check the ``*_integration_tests.py`` files in the `tests source`_ to see which variables
|
Check the ``*_integration_tests.py`` files in the `tests source`_ to see which variables
|
||||||
are required for each ESP. Depending on the supported features, the integration tests for
|
are required for each ESP. Depending on the supported features, the integration tests for
|
||||||
a particular ESP send around 5-15 individual messages. For ESPs that don't offer a sandbox,
|
a particular ESP send around 5-15 individual messages. For ESPs that don't offer a sandbox,
|
||||||
these will be real sends charged to your account (again, see the notes in each test case).
|
these will be real sends charged to your account (again, see the notes in each test case).
|
||||||
Be sure to specify a particular testenv with tox's `-e` option, or tox may repeat the tests
|
Be sure to specify a particular testenv with tox's :shell:`-e` option, or tox will repeat the tests
|
||||||
for all 20+ supported combinations of Python and Django, sending hundreds of messages.
|
for all 20+ supported combinations of Python and Django, sending hundreds of messages.
|
||||||
|
|
||||||
|
|
||||||
.. _pyenv: https://github.com/pyenv/pyenv
|
.. _pyenv: https://github.com/pyenv/pyenv
|
||||||
.. _tested via GitHub Actions: https://github.com/anymail/django-anymail/actions?query=workflow:test
|
.. _tested via GitHub Actions: https://github.com/anymail/django-anymail/actions?query=workflow:test
|
||||||
.. _tests source: https://github.com/anymail/django-anymail/blob/main/tests
|
.. _tests source: https://github.com/anymail/django-anymail/blob/main/tests
|
||||||
.. _.travis.yml: https://github.com/anymail/django-anymail/blob/main/.travis.yml
|
|
||||||
|
|
||||||
|
|
||||||
Documentation
|
Documentation
|
||||||
@@ -155,14 +148,14 @@ It's easiest to build Anymail's docs using tox:
|
|||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
|
|
||||||
$ pip install tox # (if you haven't already)
|
$ python -m pip install -r requirements-dev.txt
|
||||||
$ tox -e docs # build the docs using Sphinx
|
$ tox -e docs # build the docs using Sphinx
|
||||||
|
|
||||||
You can run Python's simple HTTP server to view them:
|
You can run Python's simple HTTP server to view them:
|
||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
|
|
||||||
$ (cd .tox/docs/_html; python3 -m http.server 8123 --bind 127.0.0.1)
|
$ (cd .tox/docs/_html; python -m http.server 8123 --bind 127.0.0.1)
|
||||||
|
|
||||||
... and then open http://localhost:8123/ in a browser. Leave the server running,
|
... and then open http://localhost:8123/ in a browser. Leave the server running,
|
||||||
and just re-run the tox command and refresh your browser as you make changes.
|
and just re-run the tox command and refresh your browser as you make changes.
|
||||||
|
|||||||
@@ -34,14 +34,21 @@ Installation
|
|||||||
------------
|
------------
|
||||||
|
|
||||||
You must ensure the :pypi:`boto3` package is installed to use Anymail's Amazon SES
|
You must ensure the :pypi:`boto3` package is installed to use Anymail's Amazon SES
|
||||||
backend. Either include the ``amazon_ses`` option when you install Anymail:
|
backend. Either include the ``amazon-ses`` option when you install Anymail:
|
||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
|
|
||||||
$ pip install "django-anymail[amazon_ses]"
|
$ pip install "django-anymail[amazon-ses]"
|
||||||
|
|
||||||
or separately run ``pip install boto3``.
|
or separately run ``pip install boto3``.
|
||||||
|
|
||||||
|
.. versionchanged:: 10.0
|
||||||
|
|
||||||
|
In earlier releases, the "extra name" could use an underscore
|
||||||
|
(``django-anymail[amazon_ses]``). That now causes pip to warn
|
||||||
|
that "django-anymail does not provide the extra 'amazon_ses'",
|
||||||
|
and may result in a broken installation that is missing boto3.
|
||||||
|
|
||||||
To send mail with Anymail's Amazon SES backend, set:
|
To send mail with Anymail's Amazon SES backend, set:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
# Packages required only for building docs
|
||||||
|
|
||||||
# (Pygments defaulted "python" to Python 2 before v2.5.0; it doesn't use semver)
|
# (Pygments defaulted "python" to Python 2 before v2.5.0; it doesn't use semver)
|
||||||
Pygments~=2.9.0
|
Pygments~=2.9.0
|
||||||
|
readme-renderer~=37.3
|
||||||
sphinx~=4.0
|
sphinx~=4.0
|
||||||
sphinx-rtd-theme~=0.5.2
|
sphinx-rtd-theme~=0.5.2
|
||||||
|
|||||||
43
hatch_build.py
Normal file
43
hatch_build.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Hatch custom build hook that generates dynamic readme.
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from hatchling.metadata.plugin.interface import MetadataHookInterface
|
||||||
|
|
||||||
|
|
||||||
|
def freeze_readme_versions(text: str, version: str) -> str:
|
||||||
|
"""
|
||||||
|
Rewrite links in readme text to refer to specific version.
|
||||||
|
(This assumes version X.Y will be tagged "vX.Y" in git.)
|
||||||
|
"""
|
||||||
|
release_tag = f"v{version}"
|
||||||
|
return re.sub(
|
||||||
|
# (?<=...) is "positive lookbehind": must be there, but won't get replaced
|
||||||
|
# GitHub Actions build status: branch=main --> branch=vX.Y.Z:
|
||||||
|
r"(?<=branch[=:])main"
|
||||||
|
# ReadTheDocs links: /stable --> /vX.Y.Z:
|
||||||
|
r"|(?<=/)stable"
|
||||||
|
# ReadTheDocs badge: version=stable --> version=vX.Y.Z:
|
||||||
|
r"|(?<=version=)stable",
|
||||||
|
release_tag,
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomMetadataHook(MetadataHookInterface):
|
||||||
|
def update(self, metadata):
|
||||||
|
"""
|
||||||
|
Update the project table's metadata.
|
||||||
|
"""
|
||||||
|
readme_path = Path(self.root) / self.config["readme"]
|
||||||
|
content_type = self.config.get("content-type", "text/x-rst")
|
||||||
|
version = metadata["version"]
|
||||||
|
|
||||||
|
readme_text = readme_path.read_text()
|
||||||
|
readme_text = freeze_readme_versions(readme_text, version)
|
||||||
|
|
||||||
|
metadata["readme"] = {
|
||||||
|
"content-type": content_type,
|
||||||
|
"text": readme_text,
|
||||||
|
}
|
||||||
100
pyproject.toml
100
pyproject.toml
@@ -1,3 +1,103 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "django-anymail"
|
||||||
|
dynamic = ["readme", "version"]
|
||||||
|
license = {file = "LICENSE"}
|
||||||
|
|
||||||
|
authors = [
|
||||||
|
{name = "Mike Edmunds", email = "medmunds@gmail.com"},
|
||||||
|
{name = "Anymail Contributors"},
|
||||||
|
]
|
||||||
|
description = """\
|
||||||
|
Django email backends and webhooks for Amazon SES, MailerSend, Mailgun, \
|
||||||
|
Mailjet, Mandrill, Postal, Postmark, SendGrid, SendinBlue, and SparkPost\
|
||||||
|
"""
|
||||||
|
# readme: see tool.hatch.metadata.hooks.custom below
|
||||||
|
keywords = [
|
||||||
|
"Django", "email", "email backend",
|
||||||
|
"ESP", "transactional mail",
|
||||||
|
"Amazon SES",
|
||||||
|
"MailerSend", "Mailgun", "Mailjet", "Mandrill",
|
||||||
|
"Postal", "Postmark",
|
||||||
|
"SendGrid", "SendinBlue", "SparkPost",
|
||||||
|
]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 5 - Production/Stable",
|
||||||
|
"Programming Language :: Python",
|
||||||
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.7",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"License :: OSI Approved :: BSD License",
|
||||||
|
"Topic :: Communications :: Email",
|
||||||
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Framework :: Django",
|
||||||
|
"Framework :: Django :: 3.0",
|
||||||
|
"Framework :: Django :: 3.1",
|
||||||
|
"Framework :: Django :: 3.2",
|
||||||
|
"Framework :: Django :: 4.0",
|
||||||
|
"Framework :: Django :: 4.1",
|
||||||
|
# not yet registered: "Framework :: Django :: 4.2",
|
||||||
|
"Environment :: Web Environment",
|
||||||
|
]
|
||||||
|
|
||||||
|
requires-python = ">=3.7"
|
||||||
|
dependencies = [
|
||||||
|
"django>=2.0",
|
||||||
|
"requests>=2.4.3",
|
||||||
|
"urllib3>=1.25.0", # requests dependency: fixes RFC 7578 header encoding
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
# ESP-specific additional dependencies.
|
||||||
|
# (For simplicity, requests is included in the base dependencies.)
|
||||||
|
# (Do not use underscores in extra names: they get normalized to hyphens.)
|
||||||
|
amazon-ses = ["boto3"]
|
||||||
|
mailersend = []
|
||||||
|
mailgun = []
|
||||||
|
mailjet = []
|
||||||
|
mandrill = []
|
||||||
|
postmark = []
|
||||||
|
sendgrid = []
|
||||||
|
sendinblue = []
|
||||||
|
sparkpost = []
|
||||||
|
postal = [
|
||||||
|
# Postal requires cryptography for verifying webhooks.
|
||||||
|
# Cryptography's wheels are broken on darwin-arm64 before Python 3.9.
|
||||||
|
"cryptography; sys_platform != 'darwin' or platform_machine != 'arm64' or python_version >= '3.9'"
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://github.com/anymail/django-anymail"
|
||||||
|
Documentation = "https://anymail.dev/en/stable/"
|
||||||
|
Source = "https://github.com/anymail/django-anymail"
|
||||||
|
Changelog = "https://anymail.dev/en/stable/changelog/"
|
||||||
|
Tracker = "https://github.com/anymail/django-anymail/issues"
|
||||||
|
|
||||||
|
[tool.hatch.build]
|
||||||
|
packages = ["anymail"]
|
||||||
|
# Hatch automatically includes pyproject.toml, LICENSE, and hatch_build.py.
|
||||||
|
# Help it find the dynamic readme source (otherwise wheel will only build with
|
||||||
|
# `hatch build`, not with `python -m build`):
|
||||||
|
force-include = {"README.rst" = "README.rst"}
|
||||||
|
|
||||||
|
[tool.hatch.metadata.hooks.custom]
|
||||||
|
# Provides dynamic readme
|
||||||
|
path = "hatch_build.py"
|
||||||
|
readme = "README.rst"
|
||||||
|
|
||||||
|
[tool.hatch.version]
|
||||||
|
path = "anymail/_version.py"
|
||||||
|
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
force-exclude = '^/tests/test_settings/settings_.*\.py'
|
force-exclude = '^/tests/test_settings/settings_.*\.py'
|
||||||
max-line-length = 88
|
max-line-length = 88
|
||||||
|
|||||||
5
requirements-dev.txt
Normal file
5
requirements-dev.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Requirements for developing (not just using) the package
|
||||||
|
|
||||||
|
hatch
|
||||||
|
pre-commit
|
||||||
|
tox<4
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
# python setup.py test
|
# usage: python runtests.py [tests.test_x tests.test_y.SomeTestCase ...]
|
||||||
# or
|
|
||||||
# runtests.py [tests.test_x tests.test_y.SomeTestCase ...]
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|||||||
134
setup.py
134
setup.py
@@ -1,134 +0,0 @@
|
|||||||
import re
|
|
||||||
from codecs import open # to use a consistent encoding
|
|
||||||
from collections import OrderedDict
|
|
||||||
from os import path
|
|
||||||
|
|
||||||
from setuptools import setup
|
|
||||||
|
|
||||||
here = path.abspath(path.dirname(__file__))
|
|
||||||
|
|
||||||
# get versions from anymail/_version.py,
|
|
||||||
# but without importing from anymail (which would break setup)
|
|
||||||
with open(path.join(here, "anymail/_version.py"), encoding="utf-8") as f:
|
|
||||||
code = compile(f.read(), "anymail/_version.py", "exec")
|
|
||||||
_version = {}
|
|
||||||
exec(code, _version)
|
|
||||||
version = _version["__version__"] # X.Y or X.Y.Z or X.Y.Z.dev1 etc.
|
|
||||||
release_tag = "v%s" % version # vX.Y or vX.Y.Z
|
|
||||||
|
|
||||||
|
|
||||||
def long_description_from_readme(rst):
|
|
||||||
# Freeze external links (on PyPI) to refer to this X.Y or X.Y.Z tag.
|
|
||||||
# (This relies on tagging releases with 'vX.Y' or 'vX.Y.Z' in GitHub.)
|
|
||||||
rst = re.sub(
|
|
||||||
# (?<=...) is "positive lookbehind": must be there, but won't get replaced
|
|
||||||
# GitHub Actions build status: branch=main --> branch=vX.Y.Z:
|
|
||||||
r"(?<=branch[=:])main"
|
|
||||||
# ReadTheDocs links: /stable --> /vX.Y.Z:
|
|
||||||
r"|(?<=/)stable"
|
|
||||||
# ReadTheDocs badge: version=stable --> version=vX.Y.Z:
|
|
||||||
r"|(?<=version=)stable",
|
|
||||||
release_tag,
|
|
||||||
rst,
|
|
||||||
)
|
|
||||||
return rst
|
|
||||||
|
|
||||||
|
|
||||||
with open(path.join(here, "README.rst"), encoding="utf-8") as f:
|
|
||||||
long_description = long_description_from_readme(f.read())
|
|
||||||
|
|
||||||
|
|
||||||
# Additional requirements for development/build/release
|
|
||||||
requirements_dev = [
|
|
||||||
"pre-commit",
|
|
||||||
"sphinx",
|
|
||||||
"sphinx-rtd-theme",
|
|
||||||
"tox",
|
|
||||||
"twine",
|
|
||||||
"wheel",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Additional requirements for running tests
|
|
||||||
requirements_test = []
|
|
||||||
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name="django-anymail",
|
|
||||||
version=version,
|
|
||||||
description=(
|
|
||||||
"Django email backends and webhooks for Amazon SES, MailerSend, Mailgun,"
|
|
||||||
" Mailjet, Mandrill, Postal, Postmark, SendGrid, SendinBlue, and SparkPost"
|
|
||||||
),
|
|
||||||
keywords=(
|
|
||||||
"Django, email, email backend, ESP, transactional mail,"
|
|
||||||
" Amazon SES, MailerSend, Mailgun, Mailjet, Mandrill, Postal, Postmark,"
|
|
||||||
" SendGrid, SendinBlue, SparkPost"
|
|
||||||
),
|
|
||||||
author="Mike Edmunds and Anymail contributors",
|
|
||||||
author_email="medmunds@gmail.com",
|
|
||||||
url="https://github.com/anymail/django-anymail",
|
|
||||||
license="BSD License",
|
|
||||||
packages=["anymail"],
|
|
||||||
zip_safe=False,
|
|
||||||
python_requires=">=3.7",
|
|
||||||
install_requires=[
|
|
||||||
"django>=2.0",
|
|
||||||
"requests>=2.4.3",
|
|
||||||
"urllib3>=1.25.0", # requests dependency: fixes RFC 7578 header encoding
|
|
||||||
],
|
|
||||||
extras_require={
|
|
||||||
# This can be used if particular backends have unique dependencies.
|
|
||||||
# For simplicity, requests is included in the base requirements.
|
|
||||||
"amazon_ses": ["boto3"],
|
|
||||||
"mailersend": [],
|
|
||||||
"mailgun": [],
|
|
||||||
"mailjet": [],
|
|
||||||
"mandrill": [],
|
|
||||||
"postmark": [],
|
|
||||||
"sendgrid": [],
|
|
||||||
"sendinblue": [],
|
|
||||||
"sparkpost": [],
|
|
||||||
"postal": ["cryptography"],
|
|
||||||
# Development/test-only requirements
|
|
||||||
# (install with python -m pip -e '.[dev,test]')
|
|
||||||
"dev": requirements_dev,
|
|
||||||
"test": requirements_test,
|
|
||||||
},
|
|
||||||
include_package_data=True,
|
|
||||||
test_suite="runtests.runtests",
|
|
||||||
tests_require=requirements_test,
|
|
||||||
classifiers=[
|
|
||||||
"Development Status :: 5 - Production/Stable",
|
|
||||||
"Programming Language :: Python",
|
|
||||||
"Programming Language :: Python :: Implementation :: PyPy",
|
|
||||||
"Programming Language :: Python :: Implementation :: CPython",
|
|
||||||
"Programming Language :: Python :: 3",
|
|
||||||
"Programming Language :: Python :: 3.7",
|
|
||||||
"Programming Language :: Python :: 3.8",
|
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
|
||||||
"Programming Language :: Python :: 3.11",
|
|
||||||
"License :: OSI Approved :: BSD License",
|
|
||||||
"Topic :: Communications :: Email",
|
|
||||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
||||||
"Intended Audience :: Developers",
|
|
||||||
"Framework :: Django",
|
|
||||||
"Framework :: Django :: 3.0",
|
|
||||||
"Framework :: Django :: 3.1",
|
|
||||||
"Framework :: Django :: 3.2",
|
|
||||||
"Framework :: Django :: 4.0",
|
|
||||||
"Framework :: Django :: 4.1",
|
|
||||||
"Framework :: Django :: 4.2",
|
|
||||||
"Environment :: Web Environment",
|
|
||||||
],
|
|
||||||
long_description=long_description,
|
|
||||||
long_description_content_type="text/x-rst",
|
|
||||||
project_urls=OrderedDict(
|
|
||||||
[
|
|
||||||
("Documentation", "https://anymail.dev/en/%s/" % release_tag),
|
|
||||||
("Source", "https://github.com/anymail/django-anymail"),
|
|
||||||
("Changelog", "https://anymail.dev/en/%s/changelog/" % release_tag),
|
|
||||||
("Tracker", "https://github.com/anymail/django-anymail/issues"),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
1
tests/requirements.txt
Normal file
1
tests/requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Additional packages needed only for running tests
|
||||||
@@ -7,6 +7,7 @@ from django.core import mail
|
|||||||
from django.core.mail import BadHeaderError
|
from django.core.mail import BadHeaderError
|
||||||
from django.test import SimpleTestCase, override_settings, tag
|
from django.test import SimpleTestCase, override_settings, tag
|
||||||
|
|
||||||
|
from anymail import __version__ as ANYMAIL_VERSION
|
||||||
from anymail.exceptions import AnymailAPIError, AnymailUnsupportedFeature
|
from anymail.exceptions import AnymailAPIError, AnymailUnsupportedFeature
|
||||||
from anymail.inbound import AnymailInboundMessage
|
from anymail.inbound import AnymailInboundMessage
|
||||||
from anymail.message import AnymailMessage, attach_inline_image_file
|
from anymail.message import AnymailMessage, attach_inline_image_file
|
||||||
@@ -812,8 +813,9 @@ class AmazonSESBackendConfigurationTests(AmazonSESBackendMockAPITestCase):
|
|||||||
config = client_params.pop("config")
|
config = client_params.pop("config")
|
||||||
# no additional params passed to session.client('ses'):
|
# no additional params passed to session.client('ses'):
|
||||||
self.assertEqual(client_params, {})
|
self.assertEqual(client_params, {})
|
||||||
self.assertRegex(
|
self.assertIn(
|
||||||
config.user_agent_extra, r"django-anymail/\d(\.\w+){1,}-amazon-ses"
|
f"django-anymail/{ANYMAIL_VERSION}-amazon-ses",
|
||||||
|
config.user_agent_extra,
|
||||||
)
|
)
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from django.core import mail
|
|||||||
from django.core.mail import BadHeaderError
|
from django.core.mail import BadHeaderError
|
||||||
from django.test import SimpleTestCase, override_settings, tag
|
from django.test import SimpleTestCase, override_settings, tag
|
||||||
|
|
||||||
|
from anymail import __version__ as ANYMAIL_VERSION
|
||||||
from anymail.exceptions import AnymailAPIError, AnymailUnsupportedFeature
|
from anymail.exceptions import AnymailAPIError, AnymailUnsupportedFeature
|
||||||
from anymail.inbound import AnymailInboundMessage
|
from anymail.inbound import AnymailInboundMessage
|
||||||
from anymail.message import AnymailMessage, attach_inline_image_file
|
from anymail.message import AnymailMessage, attach_inline_image_file
|
||||||
@@ -871,8 +872,9 @@ class AmazonSESBackendConfigurationTests(AmazonSESBackendMockAPITestCase):
|
|||||||
config = client_params.pop("config")
|
config = client_params.pop("config")
|
||||||
# no additional params passed to session.client('ses'):
|
# no additional params passed to session.client('ses'):
|
||||||
self.assertEqual(client_params, {})
|
self.assertEqual(client_params, {})
|
||||||
self.assertRegex(
|
self.assertIn(
|
||||||
config.user_agent_extra, r"django-anymail/\d(\.\w+){1,}-amazon-ses"
|
f"django-anymail/{ANYMAIL_VERSION}-amazon-ses",
|
||||||
|
config.user_agent_extra,
|
||||||
)
|
)
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
|
|||||||
@@ -120,10 +120,10 @@ class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
|
|||||||
"attachment": [
|
"attachment": [
|
||||||
{
|
{
|
||||||
"name": "attachment1.txt",
|
"name": "attachment1.txt",
|
||||||
# URL where Sendinblue can download
|
# URL where Sendinblue can download the attachment content while
|
||||||
# the attachment content while sending:
|
# sending (must be content-type: text/plain):
|
||||||
"url": "https://raw.githubusercontent.com/anymail"
|
"url": "https://raw.githubusercontent.com/anymail/django-anymail/"
|
||||||
"/django-anymail/main/AUTHORS.txt",
|
"main/docs/_readme/template.txt",
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
33
tox.ini
33
tox.ini
@@ -28,9 +28,15 @@ envlist =
|
|||||||
djangoDev-py{310,311}-all
|
djangoDev-py{310,311}-all
|
||||||
# ... then partial installation (limit extras):
|
# ... then partial installation (limit extras):
|
||||||
django42-py311-{none,amazon_ses,postal}
|
django42-py311-{none,amazon_ses,postal}
|
||||||
|
# tox requires isolated builds to use pyproject.toml build config:
|
||||||
|
isolated_build = True
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
|
args_are_paths = false
|
||||||
|
# Download latest version of pip/setuptools available on each Python version:
|
||||||
|
download = true
|
||||||
deps =
|
deps =
|
||||||
|
-rtests/requirements.txt
|
||||||
django30: django~=3.0.0
|
django30: django~=3.0.0
|
||||||
django31: django~=3.1.0
|
django31: django~=3.1.0
|
||||||
django32: django~=3.2.0
|
django32: django~=3.2.0
|
||||||
@@ -40,10 +46,10 @@ deps =
|
|||||||
django50: django~=5.0.0a0
|
django50: django~=5.0.0a0
|
||||||
djangoDev: https://github.com/django/django/tarball/main
|
djangoDev: https://github.com/django/django/tarball/main
|
||||||
extras =
|
extras =
|
||||||
# install [test] extras, unconditionally
|
# Install [esp-name] extras only when testing "all" or esp_name factor.
|
||||||
test
|
# (Only ESPs with extra dependencies need to be listed here.
|
||||||
# install [esp_name] extras only when testing "all" or esp_name factor
|
# Careful: tox factors (on the left) use underscore; extra names use hyphen.)
|
||||||
all,amazon_ses: amazon_ses
|
all,amazon_ses: amazon-ses
|
||||||
all,postal: postal
|
all,postal: postal
|
||||||
setenv =
|
setenv =
|
||||||
# tell runtests.py to limit some test tags based on extras factor
|
# tell runtests.py to limit some test tags based on extras factor
|
||||||
@@ -61,11 +67,9 @@ setenv =
|
|||||||
ignore_outcome =
|
ignore_outcome =
|
||||||
# CI that wants to handle errors itself can set TOX_OVERRIDE_IGNORE_OUTCOME=false
|
# CI that wants to handle errors itself can set TOX_OVERRIDE_IGNORE_OUTCOME=false
|
||||||
djangoDev: {env:TOX_OVERRIDE_IGNORE_OUTCOME:true}
|
djangoDev: {env:TOX_OVERRIDE_IGNORE_OUTCOME:true}
|
||||||
args_are_paths = false
|
|
||||||
# Upgrade pip/wheel/setuptools:
|
|
||||||
download = true
|
|
||||||
commands_pre =
|
commands_pre =
|
||||||
python -VV
|
python -VV
|
||||||
|
python -m pip --version
|
||||||
python -c 'import django; print("Django", django.__version__)'
|
python -c 'import django; print("Django", django.__version__)'
|
||||||
commands =
|
commands =
|
||||||
python runtests.py {posargs}
|
python runtests.py {posargs}
|
||||||
@@ -98,25 +102,22 @@ commands =
|
|||||||
pre-commit run --all-files
|
pre-commit run --all-files
|
||||||
|
|
||||||
[testenv:docs]
|
[testenv:docs]
|
||||||
basepython = python3.8
|
basepython = python3.10
|
||||||
skip_install = true
|
|
||||||
passenv =
|
passenv =
|
||||||
CONTINUOUS_INTEGRATION
|
CONTINUOUS_INTEGRATION
|
||||||
# (but not any of the live test API keys)
|
# (but not any of the live test API keys)
|
||||||
setenv =
|
setenv =
|
||||||
DOCS_BUILD_DIR={envdir}/_html
|
DOCS_BUILD_DIR={envdir}/_html
|
||||||
whitelist_externals = /bin/bash
|
|
||||||
deps =
|
deps =
|
||||||
-rdocs/requirements.txt
|
-rdocs/requirements.txt
|
||||||
commands_pre =
|
commands_pre =
|
||||||
python -VV
|
python -VV
|
||||||
sphinx-build --version
|
sphinx-build --version
|
||||||
commands =
|
commands =
|
||||||
# Verify README.rst as used in setup.py long_description:
|
|
||||||
python setup.py check --restructuredtext --strict
|
|
||||||
# Build and verify docs:
|
# Build and verify docs:
|
||||||
sphinx-build -W -b dirhtml docs {env:DOCS_BUILD_DIR}
|
sphinx-build -W -b dirhtml docs {env:DOCS_BUILD_DIR}
|
||||||
# Build README.rst into html:
|
# Build and verify package metadata readme.
|
||||||
/bin/bash -c 'python setup.py --long-description \
|
# Errors here are in README.rst:
|
||||||
| rst2html5.py --config=docs/_readme/docutils.cfg \
|
python docs/_readme/render.py \
|
||||||
> {env:DOCS_BUILD_DIR}/readme.html'
|
--package django-anymail \
|
||||||
|
--out {env:DOCS_BUILD_DIR}/readme.html
|
||||||
|
|||||||
Reference in New Issue
Block a user