sendery-django 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sendery contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.4
2
+ Name: sendery-django
3
+ Version: 0.1.0
4
+ Summary: Sendery template email client for Django
5
+ License-Expression: MIT
6
+ Project-URL: Documentation, https://sendery.co/en/docs/django
7
+ Project-URL: Repository, https://github.com/sendery-co/sendery-django
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: sendery<0.2,>=0.1
12
+ Requires-Dist: Django<5.3,>=5.2
13
+ Dynamic: license-file
14
+
15
+ # Sendery for Django
16
+
17
+ Send Sendery templates through Django’s email backend.
18
+
19
+ [Documentation](https://sendery.co/en/docs/django) · [API reference](https://sendery.co/en/docs/send-email) · [Changelog](CHANGELOG.md)
20
+
21
+ ## Requirements
22
+
23
+ Django 5.2 and Python 3.10+.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install sendery-django
29
+ ```
30
+
31
+ ## Configure the backend
32
+
33
+ Publish a `welcome` template with `name` and `action_url` variables, and create a [project API key](https://sendery.co/en/docs/authentication). Store it as `SENDERY_API_KEY` on your server. Add the backend and key to your Django settings.
34
+
35
+ ```python
36
+ # settings.py
37
+ import os
38
+
39
+ EMAIL_BACKEND = "sendery_django.backend.EmailBackend"
40
+ SENDERY_API_KEY = os.environ["SENDERY_API_KEY"]
41
+ ```
42
+
43
+ ## Send an email
44
+
45
+ Use `TemplateEmail` with one recipient. Its `sendery_receipt` contains the accepted email’s ID and status. Ordinary `EmailMessage` objects, attachments, and `cc` or `bcc` recipients are not supported. Set the sender in your Sendery project.
46
+
47
+ ```python
48
+ from sendery_django import TemplateEmail
49
+
50
+ email = TemplateEmail(
51
+ to="alex@example.com",
52
+ template="welcome",
53
+ data={"name": "Alex", "action_url": "https://example.com/start"},
54
+ )
55
+ email.send(fail_silently=False)
56
+
57
+ print(email.sendery_receipt["id"])
58
+ ```
59
+
60
+ ## Password resets
61
+
62
+ Publish a `password-reset` template with `name` and `action_url`. Use `SenderyPasswordResetForm` with Django’s authentication views. Add these routes to your `urlpatterns` and provide Django’s standard password-reset HTML pages under `templates/registration/`. If the routes already exist, change only the reset view’s `form_class`.
63
+
64
+ ```python
65
+ # urls.py
66
+ from django.contrib.auth import views as auth_views
67
+ from django.urls import path
68
+ from sendery_django.forms import SenderyPasswordResetForm
69
+
70
+ urlpatterns = [
71
+ path("password-reset/", auth_views.PasswordResetView.as_view(
72
+ form_class=SenderyPasswordResetForm,
73
+ ), name="password_reset"),
74
+ path("password-reset/done/", auth_views.PasswordResetDoneView.as_view(),
75
+ name="password_reset_done"),
76
+ path("reset/<uidb64>/<token>/", auth_views.PasswordResetConfirmView.as_view(),
77
+ name="password_reset_confirm"),
78
+ path("reset/done/", auth_views.PasswordResetCompleteView.as_view(),
79
+ name="password_reset_complete"),
80
+ ]
81
+ ```
82
+
83
+ ## Retry a send
84
+
85
+ Keep `fail_silently=False` to receive [`SenderyError`](https://sendery.co/en/docs/python) on API failures. The backend makes one attempt. In a [background task](https://sendery.co/en/docs/queues), save the recipient, variables, and `idempotency_key` before sending and reuse them for retries.
86
+
87
+ ```python
88
+ # Use the same saved recipient, variables, and event key on every attempt.
89
+ email = TemplateEmail(
90
+ to="alex@example.com",
91
+ template="welcome",
92
+ data={"name": "Alex", "action_url": "https://example.com/start"},
93
+ idempotency_key="welcome-123",
94
+ )
95
+ email.send(fail_silently=False)
96
+ ```
97
+
98
+ ## More
99
+
100
+ See [idempotency and retries](https://sendery.co/en/docs/idempotency) for retry conditions, delays, and reusing a key across attempts.
101
+
102
+ ## License
103
+
104
+ [MIT](LICENSE).
@@ -0,0 +1,90 @@
1
+ # Sendery for Django
2
+
3
+ Send Sendery templates through Django’s email backend.
4
+
5
+ [Documentation](https://sendery.co/en/docs/django) · [API reference](https://sendery.co/en/docs/send-email) · [Changelog](CHANGELOG.md)
6
+
7
+ ## Requirements
8
+
9
+ Django 5.2 and Python 3.10+.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install sendery-django
15
+ ```
16
+
17
+ ## Configure the backend
18
+
19
+ Publish a `welcome` template with `name` and `action_url` variables, and create a [project API key](https://sendery.co/en/docs/authentication). Store it as `SENDERY_API_KEY` on your server. Add the backend and key to your Django settings.
20
+
21
+ ```python
22
+ # settings.py
23
+ import os
24
+
25
+ EMAIL_BACKEND = "sendery_django.backend.EmailBackend"
26
+ SENDERY_API_KEY = os.environ["SENDERY_API_KEY"]
27
+ ```
28
+
29
+ ## Send an email
30
+
31
+ Use `TemplateEmail` with one recipient. Its `sendery_receipt` contains the accepted email’s ID and status. Ordinary `EmailMessage` objects, attachments, and `cc` or `bcc` recipients are not supported. Set the sender in your Sendery project.
32
+
33
+ ```python
34
+ from sendery_django import TemplateEmail
35
+
36
+ email = TemplateEmail(
37
+ to="alex@example.com",
38
+ template="welcome",
39
+ data={"name": "Alex", "action_url": "https://example.com/start"},
40
+ )
41
+ email.send(fail_silently=False)
42
+
43
+ print(email.sendery_receipt["id"])
44
+ ```
45
+
46
+ ## Password resets
47
+
48
+ Publish a `password-reset` template with `name` and `action_url`. Use `SenderyPasswordResetForm` with Django’s authentication views. Add these routes to your `urlpatterns` and provide Django’s standard password-reset HTML pages under `templates/registration/`. If the routes already exist, change only the reset view’s `form_class`.
49
+
50
+ ```python
51
+ # urls.py
52
+ from django.contrib.auth import views as auth_views
53
+ from django.urls import path
54
+ from sendery_django.forms import SenderyPasswordResetForm
55
+
56
+ urlpatterns = [
57
+ path("password-reset/", auth_views.PasswordResetView.as_view(
58
+ form_class=SenderyPasswordResetForm,
59
+ ), name="password_reset"),
60
+ path("password-reset/done/", auth_views.PasswordResetDoneView.as_view(),
61
+ name="password_reset_done"),
62
+ path("reset/<uidb64>/<token>/", auth_views.PasswordResetConfirmView.as_view(),
63
+ name="password_reset_confirm"),
64
+ path("reset/done/", auth_views.PasswordResetCompleteView.as_view(),
65
+ name="password_reset_complete"),
66
+ ]
67
+ ```
68
+
69
+ ## Retry a send
70
+
71
+ Keep `fail_silently=False` to receive [`SenderyError`](https://sendery.co/en/docs/python) on API failures. The backend makes one attempt. In a [background task](https://sendery.co/en/docs/queues), save the recipient, variables, and `idempotency_key` before sending and reuse them for retries.
72
+
73
+ ```python
74
+ # Use the same saved recipient, variables, and event key on every attempt.
75
+ email = TemplateEmail(
76
+ to="alex@example.com",
77
+ template="welcome",
78
+ data={"name": "Alex", "action_url": "https://example.com/start"},
79
+ idempotency_key="welcome-123",
80
+ )
81
+ email.send(fail_silently=False)
82
+ ```
83
+
84
+ ## More
85
+
86
+ See [idempotency and retries](https://sendery.co/en/docs/idempotency) for retry conditions, delays, and reusing a key across attempts.
87
+
88
+ ## License
89
+
90
+ [MIT](LICENSE).
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sendery-django"
7
+ version = "0.1.0"
8
+ description = "Sendery template email client for Django"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ dependencies = ["sendery>=0.1,<0.2", "Django>=5.2,<5.3"]
14
+
15
+ [tool.setuptools.packages.find]
16
+ where = ["src"]
17
+
18
+ [project.urls]
19
+ Documentation = "https://sendery.co/en/docs/django"
20
+ Repository = "https://github.com/sendery-co/sendery-django"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,11 @@
1
+ """Sendery integration for Django."""
2
+ from django.core.mail import EmailMessage
3
+ import json
4
+ import uuid
5
+
6
+
7
+ class TemplateEmail(EmailMessage):
8
+ def __init__(self, *, to, template, data, locale=None, idempotency_key=None, **kwargs):
9
+ super().__init__(to=[to], **kwargs)
10
+ self.sendery_payload = json.loads(json.dumps({"template": template, "data": data, "locale": locale}, allow_nan=False))
11
+ self.sendery_key = idempotency_key if idempotency_key is not None else str(uuid.uuid4())
@@ -0,0 +1,20 @@
1
+ from django.conf import settings
2
+ from django.core.mail.backends.base import BaseEmailBackend
3
+ from sendery import Sendery, SenderyError
4
+ from . import TemplateEmail
5
+
6
+
7
+ class EmailBackend(BaseEmailBackend):
8
+ def send_messages(self, email_messages):
9
+ client = Sendery(settings.SENDERY_API_KEY, getattr(settings, "SENDERY_URL", "https://sendery.co"))
10
+ sent = 0
11
+ for message in email_messages or []:
12
+ if not isinstance(message, TemplateEmail) or len(message.to) != 1 or message.cc or message.bcc or message.attachments:
13
+ raise ValueError("Use TemplateEmail with exactly one recipient and no attachments.")
14
+ try:
15
+ message.sendery_receipt = client.send(to=message.to[0], **message.sendery_payload, idempotency_key=message.sendery_key)
16
+ sent += 1
17
+ except SenderyError:
18
+ if not self.fail_silently:
19
+ raise
20
+ return sent
@@ -0,0 +1,12 @@
1
+ from django.contrib.auth.forms import PasswordResetForm
2
+ from django.urls import reverse
3
+ from . import TemplateEmail
4
+
5
+
6
+ class SenderyPasswordResetForm(PasswordResetForm):
7
+ template_key = "password-reset"
8
+
9
+ def send_mail(self, subject_template_name, email_template_name, context, from_email, to_email, html_email_template_name=None):
10
+ path = reverse("password_reset_confirm", kwargs={"uidb64": context["uid"], "token": context["token"]})
11
+ url = f"{context['protocol']}://{context['domain']}{path}"
12
+ TemplateEmail(to=to_email, template=self.template_key, data={"name": context["user"].get_username(), "action_url": url}, from_email=from_email).send()
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.4
2
+ Name: sendery-django
3
+ Version: 0.1.0
4
+ Summary: Sendery template email client for Django
5
+ License-Expression: MIT
6
+ Project-URL: Documentation, https://sendery.co/en/docs/django
7
+ Project-URL: Repository, https://github.com/sendery-co/sendery-django
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: sendery<0.2,>=0.1
12
+ Requires-Dist: Django<5.3,>=5.2
13
+ Dynamic: license-file
14
+
15
+ # Sendery for Django
16
+
17
+ Send Sendery templates through Django’s email backend.
18
+
19
+ [Documentation](https://sendery.co/en/docs/django) · [API reference](https://sendery.co/en/docs/send-email) · [Changelog](CHANGELOG.md)
20
+
21
+ ## Requirements
22
+
23
+ Django 5.2 and Python 3.10+.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install sendery-django
29
+ ```
30
+
31
+ ## Configure the backend
32
+
33
+ Publish a `welcome` template with `name` and `action_url` variables, and create a [project API key](https://sendery.co/en/docs/authentication). Store it as `SENDERY_API_KEY` on your server. Add the backend and key to your Django settings.
34
+
35
+ ```python
36
+ # settings.py
37
+ import os
38
+
39
+ EMAIL_BACKEND = "sendery_django.backend.EmailBackend"
40
+ SENDERY_API_KEY = os.environ["SENDERY_API_KEY"]
41
+ ```
42
+
43
+ ## Send an email
44
+
45
+ Use `TemplateEmail` with one recipient. Its `sendery_receipt` contains the accepted email’s ID and status. Ordinary `EmailMessage` objects, attachments, and `cc` or `bcc` recipients are not supported. Set the sender in your Sendery project.
46
+
47
+ ```python
48
+ from sendery_django import TemplateEmail
49
+
50
+ email = TemplateEmail(
51
+ to="alex@example.com",
52
+ template="welcome",
53
+ data={"name": "Alex", "action_url": "https://example.com/start"},
54
+ )
55
+ email.send(fail_silently=False)
56
+
57
+ print(email.sendery_receipt["id"])
58
+ ```
59
+
60
+ ## Password resets
61
+
62
+ Publish a `password-reset` template with `name` and `action_url`. Use `SenderyPasswordResetForm` with Django’s authentication views. Add these routes to your `urlpatterns` and provide Django’s standard password-reset HTML pages under `templates/registration/`. If the routes already exist, change only the reset view’s `form_class`.
63
+
64
+ ```python
65
+ # urls.py
66
+ from django.contrib.auth import views as auth_views
67
+ from django.urls import path
68
+ from sendery_django.forms import SenderyPasswordResetForm
69
+
70
+ urlpatterns = [
71
+ path("password-reset/", auth_views.PasswordResetView.as_view(
72
+ form_class=SenderyPasswordResetForm,
73
+ ), name="password_reset"),
74
+ path("password-reset/done/", auth_views.PasswordResetDoneView.as_view(),
75
+ name="password_reset_done"),
76
+ path("reset/<uidb64>/<token>/", auth_views.PasswordResetConfirmView.as_view(),
77
+ name="password_reset_confirm"),
78
+ path("reset/done/", auth_views.PasswordResetCompleteView.as_view(),
79
+ name="password_reset_complete"),
80
+ ]
81
+ ```
82
+
83
+ ## Retry a send
84
+
85
+ Keep `fail_silently=False` to receive [`SenderyError`](https://sendery.co/en/docs/python) on API failures. The backend makes one attempt. In a [background task](https://sendery.co/en/docs/queues), save the recipient, variables, and `idempotency_key` before sending and reuse them for retries.
86
+
87
+ ```python
88
+ # Use the same saved recipient, variables, and event key on every attempt.
89
+ email = TemplateEmail(
90
+ to="alex@example.com",
91
+ template="welcome",
92
+ data={"name": "Alex", "action_url": "https://example.com/start"},
93
+ idempotency_key="welcome-123",
94
+ )
95
+ email.send(fail_silently=False)
96
+ ```
97
+
98
+ ## More
99
+
100
+ See [idempotency and retries](https://sendery.co/en/docs/idempotency) for retry conditions, delays, and reusing a key across attempts.
101
+
102
+ ## License
103
+
104
+ [MIT](LICENSE).
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/sendery_django/__init__.py
5
+ src/sendery_django/backend.py
6
+ src/sendery_django/forms.py
7
+ src/sendery_django.egg-info/PKG-INFO
8
+ src/sendery_django.egg-info/SOURCES.txt
9
+ src/sendery_django.egg-info/dependency_links.txt
10
+ src/sendery_django.egg-info/requires.txt
11
+ src/sendery_django.egg-info/top_level.txt
12
+ tests/test_backend.py
@@ -0,0 +1,2 @@
1
+ sendery<0.2,>=0.1
2
+ Django<5.3,>=5.2
@@ -0,0 +1 @@
1
+ sendery_django
@@ -0,0 +1,46 @@
1
+ import pickle
2
+ import unittest
3
+ from unittest.mock import patch
4
+ from django.conf import settings
5
+ if not settings.configured:
6
+ settings.configure(SECRET_KEY="test-only", ROOT_URLCONF=__name__, INSTALLED_APPS=["django.contrib.auth", "django.contrib.contenttypes"], SENDERY_API_KEY="test", EMAIL_BACKEND="sendery_django.backend.EmailBackend", DEFAULT_FROM_EMAIL="hello@example.com")
7
+ import django
8
+ django.setup()
9
+ from django.urls import path
10
+ from django.http import HttpResponse
11
+ urlpatterns = [path("reset/<uidb64>/<token>/", lambda request, **kwargs: HttpResponse(), name="password_reset_confirm")]
12
+ from django.core.mail import EmailMessage
13
+ from sendery_django import TemplateEmail
14
+ from sendery_django.backend import EmailBackend
15
+
16
+
17
+ class BackendTests(unittest.TestCase):
18
+ def test_queue_serialization_preserves_key_and_data(self):
19
+ data = {"name": "Original"}
20
+ email = TemplateEmail(to="alex@example.com", template="welcome", data=data)
21
+ data["name"] = "Changed"
22
+ restored = pickle.loads(pickle.dumps(email))
23
+ with patch("sendery_django.backend.Sendery") as client:
24
+ self.assertEqual(1, EmailBackend().send_messages([restored]))
25
+ payload = client.return_value.send.call_args.kwargs
26
+ self.assertEqual(email.sendery_key, payload["idempotency_key"])
27
+ self.assertEqual("Original", payload["data"]["name"])
28
+
29
+ def test_rejects_ordinary_emails_and_multiple_recipients(self):
30
+ with self.assertRaises(ValueError):
31
+ EmailBackend().send_messages([EmailMessage("subject", "body", to=["a@example.com"])])
32
+ email = TemplateEmail(to="a@example.com", template="welcome", data={})
33
+ email.to.append("b@example.com")
34
+ with self.assertRaises(ValueError):
35
+ EmailBackend().send_messages([email])
36
+
37
+ def test_password_reset_keeps_django_token_and_url(self):
38
+ from sendery_django.forms import SenderyPasswordResetForm
39
+ from types import SimpleNamespace
40
+ user = SimpleNamespace(get_username=lambda: "Alex")
41
+ context = {"uid": "MTI", "token": "signed-token", "protocol": "https", "domain": "product.example", "user": user}
42
+ with patch("sendery_django.forms.TemplateEmail") as message:
43
+ SenderyPasswordResetForm().send_mail("subject", "body", context, "hello@example.com", "alex@example.com")
44
+ self.assertEqual("https://product.example/reset/MTI/signed-token/", message.call_args.kwargs["data"]["action_url"])
45
+ self.assertEqual("password-reset", message.call_args.kwargs["template"])
46
+ message.return_value.send.assert_called_once()