Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrated to Asphalt 5 #48

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Create packages
run: python -m build
- name: Archive packages
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: dist
path: dist
Expand All @@ -36,7 +36,7 @@ jobs:
id-token: write
steps:
- name: Retrieve packages
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
- name: Upload packages
uses: pypa/gh-action-pypi-publish@release/v1

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "pypy-3.10"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "pypy-3.10"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -22,7 +22,7 @@ jobs:
cache: pip
cache-dependency-path: pyproject.toml
- name: Install dependencies
run: pip install .[test]
run: pip install -e .[test]
- name: Test with pytest
run: coverage run -m pytest -v
- name: Generate coverage report
Expand Down
18 changes: 12 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# * Run "pre-commit install".
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: check-toml
- id: check-yaml
Expand All @@ -16,20 +16,26 @@ repos:
- id: trailing-whitespace

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.5
rev: v0.8.4
hooks:
- id: ruff
args: [--fix, --show-fixes]
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
rev: v1.14.0
hooks:
- id: mypy
args: ["--explicit-package-bases"]
additional_dependencies:
- aiosmtpd
- aiosmtplib
- asphalt
- asphalt@git+https://github.com/asphalt-framework/asphalt
- pytest
- smtpproto >= 2.0
- trustme

- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
- id: rst-backticks
- id: rst-directive-colons
- id: rst-inline-touching-normal
2 changes: 1 addition & 1 deletion .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: 2
build:
os: ubuntu-22.04
tools:
python: "3.8"
python: "3.9"

sphinx:
configuration: docs/conf.py
Expand Down
27 changes: 9 additions & 18 deletions docs/api.rst
Original file line number Diff line number Diff line change
@@ -1,40 +1,31 @@
API reference
=============

.. py:currentmodule:: asphalt.mailer

Component
---------

.. automodule:: asphalt.mailer.component
:members:
.. autoclass:: MailerComponent

Interfaces
----------

.. autoclass:: asphalt.mailer.api.Mailer
:members:
.. autoclass:: Mailer

Exceptions
----------

.. autoexception:: asphalt.mailer.api.DeliveryError
.. autoexception:: DeliveryError

Utilities
---------

.. automodule:: asphalt.mailer.utils
:members:
.. autofunction:: get_recipients

Mailer back-ends
----------------

.. automodule:: asphalt.mailer.mailers.smtp
:members:
:show-inheritance:

.. automodule:: asphalt.mailer.mailers.sendmail
:members:
:show-inheritance:

.. automodule:: asphalt.mailer.mailers.mock
:members:
:show-inheritance:
.. autoclass:: asphalt.mailer.mailers.smtp.SMTPMailer
.. autoclass:: asphalt.mailer.mailers.sendmail.SendmailMailer
.. autoclass:: asphalt.mailer.mailers.mock.MockMailer
2 changes: 2 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx_autodoc_typehints",
"sphinx_rtd_theme",
]

templates_path = ["_templates"]
Expand All @@ -25,6 +26,7 @@
exclude_patterns = ["_build"]
pygments_style = "sphinx"
autodoc_default_options = {"members": True, "show-inheritance": True}
autodoc_inherit_docstrings = False
highlight_language = "python3"
todo_include_todos = False

Expand Down
33 changes: 15 additions & 18 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ Configuration
.. highlight:: yaml
.. py:currentmodule:: asphalt.mailer

To configure a mailer for your application, you need to choose a backend and then specify
any necessary configuration values for it. The following backends are provided out of the box:
To configure a mailer for your application, you need to choose a backend and then
specify any necessary configuration values for it. The following backends are provided
out of the box:

* :mod:`~.mailers.smtp` (**recommended**)
* :mod:`~.mailers.sendmail`
* :mod:`~.mailers.mock` (for testing only)

Other backends may be provided by other components.

Once you've selected a backend, see its specific documentation to find out what configuration
values you need to provide, if any. Configuration values are expressed as constructor arguments
for the backend class:
Once you've selected a backend, see its specific documentation to find out what
configuration values you need to provide, if any. Configuration values are expressed as
initializer arguments for the backend class:

.. code-block:: yaml

Expand All @@ -26,18 +27,14 @@ for the backend class:
username: foo
password: bar

This configuration uses ``primary-smtp.company.com`` as the server hostname. Because it has a
user name and password defined, the mailer will automatically use port 587 and STARTTLS_ before
authenticating itself with the server.
This configuration uses ``primary-smtp.company.com`` as the server hostname. Because it
has a user name and password defined, the mailer will automatically use port 587 and
STARTTLS_ before authenticating itself with the server.

The above configuration can be done directly in Python code as follows::
The above configuration can be done directly in Python code as follows:

class ApplicationComponent(ContainerComponent):
async def start(ctx: Context):
self.add_component(
'mailer', backend='smtp', host='primary-smtp.company.com', username='foo',
password='bar')
await super().start()
.. literalinclude:: snippets/configuration1.py
:language: python

.. _STARTTLS: https://en.wikipedia.org/wiki/Opportunistic_TLS

Expand All @@ -55,8 +52,8 @@ of the mailer component:
host: primary-smtp.company.com
username: foo
password: dummypass
mailer2:
type: mailer
mailer/alternate:
resource_name: alternate
backend: sendmail

The above configuration creates two mailer resources: ``mailer`` and ``mailer2``.
The above configuration creates two mailer resources: ``default`` and ``alternate``.
41 changes: 20 additions & 21 deletions docs/extending.rst
Original file line number Diff line number Diff line change
@@ -1,32 +1,31 @@
Writing new mailer backends
===========================

If you wish to implement an alternate method of sending email, you can do so by subclassing the
:class:`~asphalt.mailer.api.Mailer` class. There are two methods implementors typically override:
.. py:currentmodule:: asphalt.mailer

* :meth:`~asphalt.mailer.api.Mailer.start` (optional)
* :meth:`~asphalt.mailer.api.Mailer.deliver`
If you wish to implement an alternate method of sending email, you can do so by
subclassing the :class:`~Mailer` class. There are two methods implementors typically
override:

The ``start`` method is a coroutine that is called by the component from its own
:meth:`~asphalt.core.component.Component.start` method. You can handle any necessary resource
* :meth:`~Mailer.start` (optional)
* :meth:`~Mailer.deliver`

The ``start`` method is a coroutine that is called by :class:`~MailerComponent` from its
own :meth:`~asphalt.core.Component.start` method. You can handle any necessary resource
related setup there.

The ``deliver`` method must be overridden and needs to:
The :meth:`~Mailer.deliver` method must be overridden and needs to:

#. handle both a single :class:`~email.message.EmailMessage` and an iterable of them
#. remove any ``Bcc`` header from each message to avoid revealing the hidden recipients

If you want your mailer to be available as a backend for the
:class:`~asphalt.mailer.component.MailerComponent`, you need to add the corresponding entry point
for it. Suppose your mailer class is named ``AwesomeMailer``, lives in the package
``foo.bar.awesome`` and you want to give it the alias ``awesome``, add this line to your project's
``setup.py`` under the ``entrypoints`` argument in the ``asphalt.mailer.mailers`` namespace::

setup(
# (...other arguments...)
entry_points={
'asphalt.mailer.mailers': [
'awesome = foo.bar.awesome:AwesomeMailer'
]
}
)
If you want your mailer to be available as a backend for the :class:`~MailerComponent`,
you need to add the corresponding entry point for it. Suppose your mailer class is named
``AwesomeMailer``, lives in the package ``foo.bar.awesome`` and you want to give it the
alias ``awesome``, add this line to your project's ``pyproject.toml`` under the
``project.entry-points`` key in the ``asphalt.mailer.mailers`` namespace:

.. code-block:: toml

[project.entry-points."asphalt.mailer.mailers"]
awesome = "foo.bar.awesome:AwesomeMailer"
12 changes: 12 additions & 0 deletions docs/snippets/configuration1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from asphalt.core import Component


class ApplicationComponent(Component):
def __init__(self) -> None:
self.add_component(
"mailer",
backend="smtp",
host="primary-smtp.company.com",
username="foo",
password="bar",
)
20 changes: 20 additions & 0 deletions docs/snippets/testing1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from typing import cast

import pytest
from asphalt.core import Context, get_resource_nowait, start_component

from asphalt.mailer import Mailer, MailerComponent
from asphalt.mailer.mailers.mock import MockMailer

pytestmark = pytest.mark.anyio


async def test_foo() -> None:
async with Context():
await start_component(MailerComponent, {"backend": "mock"})
mailer = cast(MockMailer, get_resource_nowait(Mailer)) # type: ignore[type-abstract]
await mailer.create_and_deliver(to="[email protected]")

# check that exactly one message was sent, to [email protected]
assert len(mailer.messages) == 1
assert mailer.messages[0]["To"] == "[email protected]"
13 changes: 13 additions & 0 deletions docs/snippets/usage1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from asphalt.core import inject, resource

from asphalt.mailer import Mailer


@inject
async def handler(*, mailer: Mailer = resource()) -> None:
await mailer.create_and_deliver(
subject="Hi there!",
sender="Example Person <[email protected]>",
to="[email protected]",
plain_body="Greetings from Example!",
)
16 changes: 16 additions & 0 deletions docs/snippets/usage2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from asphalt.core import inject, resource

from asphalt.mailer import Mailer


@inject
async def handler(*, mailer: Mailer = resource()) -> None:
html = "<h1>Greetings</h1>Greetings from <strong>Example Person!</strong>"
plain = "Greetings!\n\nGreetings from Example Person!"
await mailer.create_and_deliver(
subject="Hi there!",
sender="Example Person <[email protected]>",
to="[email protected]",
plain_body=plain,
html_body=html,
)
15 changes: 15 additions & 0 deletions docs/snippets/usage3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from asphalt.core import inject, resource

from asphalt.mailer import Mailer


@inject
async def handler(*, mailer: Mailer = resource()) -> None:
message = mailer.create_message(
subject="Hi there!",
sender="Example Person <[email protected]>",
to="[email protected]",
plain_body="See the attached file.",
)
await mailer.add_file_attachment(message, "/path/to/file.zip")
await mailer.deliver(message)
15 changes: 15 additions & 0 deletions docs/snippets/usage4.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from asphalt.core import inject, resource

from asphalt.mailer import Mailer


@inject
async def handler(*, mailer: Mailer = resource()) -> None:
message = mailer.create_message(
subject="Hi there!",
sender="Example Person <[email protected]>",
to="[email protected]",
plain_body="See the attached file.",
)
mailer.add_attachment(message, b"file contents", "attachment.txt")
await mailer.deliver(message)
23 changes: 23 additions & 0 deletions docs/snippets/usage5.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from email.headerregistry import Address

from asphalt.core import inject, resource

from asphalt.mailer import Mailer


@inject
async def handler(*, mailer: Mailer = resource()) -> None:
messages = []
for recipient in [
Address("Some Person", "some.person", "company.com"),
Address("Other Person", "other.person", "company.com"),
]:
message = mailer.create_message(
subject=f"Hi there, {recipient.display_name}!",
sender="Example Person <[email protected]>",
to=recipient,
plain_body=f"How are you doing, {recipient.display_name}?",
)
messages.append(message)

await mailer.deliver(messages)
Loading
Loading