postmarker 1.0__tar.gz → 2.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.
- postmarker-2.0/.gitignore +94 -0
- postmarker-2.0/PKG-INFO +146 -0
- postmarker-2.0/README.md +95 -0
- postmarker-2.0/pyproject.toml +127 -0
- postmarker-2.0/src/postmarker/__init__.py +1 -0
- {postmarker-1.0 → postmarker-2.0}/src/postmarker/core.py +60 -24
- postmarker-2.0/src/postmarker/django/__init__.py +12 -0
- {postmarker-1.0 → postmarker-2.0}/src/postmarker/django/backend.py +27 -16
- {postmarker-1.0 → postmarker-2.0}/src/postmarker/exceptions.py +6 -1
- {postmarker-1.0 → postmarker-2.0}/src/postmarker/logging.py +4 -1
- postmarker-2.0/src/postmarker/models/base.py +134 -0
- {postmarker-1.0 → postmarker-2.0}/src/postmarker/models/bounces.py +43 -22
- postmarker-2.0/src/postmarker/models/domains.py +75 -0
- {postmarker-1.0 → postmarker-2.0}/src/postmarker/models/emails.py +185 -141
- postmarker-2.0/src/postmarker/models/message_streams.py +95 -0
- {postmarker-1.0 → postmarker-2.0}/src/postmarker/models/messages.py +153 -76
- postmarker-2.0/src/postmarker/models/senders.py +106 -0
- {postmarker-1.0 → postmarker-2.0}/src/postmarker/models/server.py +9 -5
- postmarker-2.0/src/postmarker/models/servers.py +55 -0
- {postmarker-1.0 → postmarker-2.0}/src/postmarker/models/stats.py +19 -15
- {postmarker-1.0 → postmarker-2.0}/src/postmarker/models/status.py +22 -15
- postmarker-2.0/src/postmarker/models/suppressions.py +95 -0
- postmarker-2.0/src/postmarker/models/templates.py +155 -0
- {postmarker-1.0 → postmarker-2.0}/src/postmarker/models/triggers.py +27 -22
- postmarker-2.0/src/postmarker/py.typed +0 -0
- postmarker-2.0/src/postmarker/pytest.py +34 -0
- {postmarker-1.0 → postmarker-2.0}/src/postmarker/tornado.py +15 -4
- {postmarker-1.0 → postmarker-2.0}/src/postmarker/utils.py +9 -3
- postmarker-2.0/test/__init__.py +0 -0
- postmarker-2.0/test/cassettes/test_exceptions/test_raise.yaml +37 -0
- postmarker-2.0/test/cassettes/test_spamcheck/test_spamcheck_invalid.yaml +41 -0
- postmarker-2.0/test/cassettes/test_spamcheck/test_spamcheck_valid.yaml +60 -0
- postmarker-2.0/test/conftest.py +239 -0
- postmarker-2.0/test/django/__init__.py +0 -0
- postmarker-2.0/test/django/conftest.py +11 -0
- postmarker-2.0/test/django/settings.py +4 -0
- postmarker-2.0/test/django/test_backend.py +552 -0
- postmarker-2.0/test/django/test_mixins.py +92 -0
- postmarker-2.0/test/models/__init__.py +0 -0
- postmarker-2.0/test/models/attachments/readme.txt +1 -0
- postmarker-2.0/test/models/attachments/report.blabla +1 -0
- postmarker-2.0/test/models/cassettes/test_bounces/test_load_all_bounces_multiple_calls[2-1-2].yaml +77 -0
- postmarker-2.0/test/models/cassettes/test_bounces/test_load_all_bounces_multiple_calls[2-50-1].yaml +42 -0
- postmarker-2.0/test/models/cassettes/test_bounces/test_load_all_bounces_multiple_calls[4-1-2].yaml +77 -0
- postmarker-2.0/test/models/cassettes/test_bounces/test_load_all_bounces_multiple_calls[None-1-2].yaml +77 -0
- postmarker-2.0/test/models/cassettes/test_bounces/test_manager_all.yaml +42 -0
- postmarker-2.0/test/models/cassettes/test_bounces/test_manager_deliverystats.yaml +38 -0
- postmarker-2.0/test/models/cassettes/test_bounces/test_manager_tags.yaml +36 -0
- postmarker-2.0/test/models/cassettes/test_bounces/test_message.yaml +78 -0
- postmarker-2.0/test/models/cassettes/test_bounces/test_model_activate.yaml +82 -0
- postmarker-2.0/test/models/cassettes/test_bounces/test_model_dump.yaml +75 -0
- postmarker-2.0/test/models/cassettes/test_bounces/test_model_get.yaml +41 -0
- postmarker-2.0/test/models/cassettes/test_bounces/test_model_repr.yaml +41 -0
- postmarker-2.0/test/models/cassettes/test_bounces/test_model_str.yaml +41 -0
- postmarker-2.0/test/models/cassettes/test_domains/test_manager_all.yaml +36 -0
- postmarker-2.0/test/models/cassettes/test_domains/test_manager_domain_methods.yaml +279 -0
- postmarker-2.0/test/models/cassettes/test_domains/test_model_get.yaml +74 -0
- postmarker-2.0/test/models/cassettes/test_domains/test_model_str.yaml +38 -0
- postmarker-2.0/test/models/cassettes/test_emails/test_simple_send_default.yaml +44 -0
- postmarker-2.0/test/models/cassettes/test_emails/test_simple_send_mime_text.yaml +43 -0
- postmarker-2.0/test/models/cassettes/test_emails/test_simple_send_minimum_mime.yaml +43 -0
- postmarker-2.0/test/models/cassettes/test_emails/test_simple_send_with_template.yaml +40 -0
- postmarker-2.0/test/models/cassettes/test_messages/test_inbound_message_bypass.yaml +39 -0
- postmarker-2.0/test/models/cassettes/test_messages/test_inbound_message_retry.yaml +39 -0
- postmarker-2.0/test/models/cassettes/test_messages/test_inbound_messages_all.yaml +36 -0
- postmarker-2.0/test/models/cassettes/test_messages/test_opens_model.yaml +37 -0
- postmarker-2.0/test/models/cassettes/test_messages/test_outbound_message.yaml +37 -0
- postmarker-2.0/test/models/cassettes/test_messages/test_outbound_message_get.yaml +75 -0
- postmarker-2.0/test/models/cassettes/test_messages/test_outbound_message_get_dump.yaml +71 -0
- postmarker-2.0/test/models/cassettes/test_messages/test_outbound_message_opens.yaml +71 -0
- postmarker-2.0/test/models/cassettes/test_senders/test_all.yaml +38 -0
- postmarker-2.0/test/models/cassettes/test_senders/test_all_methods.yaml +190 -0
- postmarker-2.0/test/models/cassettes/test_senders/test_edit.yaml +92 -0
- postmarker-2.0/test/models/cassettes/test_senders/test_get.yaml +88 -0
- postmarker-2.0/test/models/cassettes/test_server/test_edit.yaml +74 -0
- postmarker-2.0/test/models/cassettes/test_server/test_str.yaml +36 -0
- postmarker-2.0/test/models/cassettes/test_stats/test_methods[bounces-kwargs2-expected2].yaml +36 -0
- postmarker-2.0/test/models/cassettes/test_stats/test_methods[browserfamilies-kwargs10-expected10].yaml +36 -0
- postmarker-2.0/test/models/cassettes/test_stats/test_methods[clicks-kwargs9-expected9].yaml +36 -0
- postmarker-2.0/test/models/cassettes/test_stats/test_methods[clicks_platforms-kwargs11-expected11].yaml +36 -0
- postmarker-2.0/test/models/cassettes/test_stats/test_methods[emailclients-kwargs7-expected7].yaml +36 -0
- postmarker-2.0/test/models/cassettes/test_stats/test_methods[location-kwargs12-expected12].yaml +36 -0
- postmarker-2.0/test/models/cassettes/test_stats/test_methods[opens-kwargs5-expected5].yaml +36 -0
- postmarker-2.0/test/models/cassettes/test_stats/test_methods[opens_platforms-kwargs6-expected6].yaml +36 -0
- postmarker-2.0/test/models/cassettes/test_stats/test_methods[overview-kwargs0-expected0].yaml +36 -0
- postmarker-2.0/test/models/cassettes/test_stats/test_methods[readtimes-kwargs8-expected8].yaml +36 -0
- postmarker-2.0/test/models/cassettes/test_stats/test_methods[sends-kwargs1-expected1].yaml +36 -0
- postmarker-2.0/test/models/cassettes/test_stats/test_methods[spam-kwargs3-expected3].yaml +36 -0
- postmarker-2.0/test/models/cassettes/test_stats/test_methods[tracked-kwargs4-expected4].yaml +36 -0
- postmarker-2.0/test/models/cassettes/test_status/test_availability.yaml +40 -0
- postmarker-2.0/test/models/cassettes/test_status/test_delivery.yaml +40 -0
- postmarker-2.0/test/models/cassettes/test_status/test_get_incident.yaml +46 -0
- postmarker-2.0/test/models/cassettes/test_status/test_incidents.yaml +346 -0
- postmarker-2.0/test/models/cassettes/test_status/test_last_incident.yaml +46 -0
- postmarker-2.0/test/models/cassettes/test_status/test_services.yaml +43 -0
- postmarker-2.0/test/models/cassettes/test_status/test_status.yaml +40 -0
- postmarker-2.0/test/models/cassettes/test_templates/test_manager_all.yaml +36 -0
- postmarker-2.0/test/models/cassettes/test_templates/test_manager_create.yaml +77 -0
- postmarker-2.0/test/models/cassettes/test_templates/test_manager_edit.yaml +40 -0
- postmarker-2.0/test/models/cassettes/test_templates/test_manager_validate.yaml +41 -0
- postmarker-2.0/test/models/cassettes/test_templates/test_model_default.yaml +40 -0
- postmarker-2.0/test/models/cassettes/test_templates/test_model_edit.yaml +78 -0
- postmarker-2.0/test/models/cassettes/test_templates/test_model_get.yaml +78 -0
- postmarker-2.0/test/models/cassettes/test_templates/test_model_repr.yaml +40 -0
- postmarker-2.0/test/models/cassettes/test_triggers/test_inbound_rules_all.yaml +36 -0
- postmarker-2.0/test/models/cassettes/test_triggers/test_inbound_rules_create.yaml +40 -0
- postmarker-2.0/test/models/cassettes/test_triggers/test_inbound_rules_delete.yaml +72 -0
- postmarker-2.0/test/models/cassettes/test_triggers/test_tags_all.yaml +36 -0
- postmarker-2.0/test/models/cassettes/test_triggers/test_tags_create.yaml +40 -0
- postmarker-2.0/test/models/cassettes/test_triggers/test_tags_delete.yaml +72 -0
- postmarker-2.0/test/models/cassettes/test_triggers/test_tags_edit.yaml +78 -0
- postmarker-2.0/test/models/cassettes/test_triggers/test_tags_get.yaml +70 -0
- postmarker-2.0/test/models/test_base.py +72 -0
- postmarker-2.0/test/models/test_bounces.py +80 -0
- postmarker-2.0/test/models/test_clicks.py +244 -0
- postmarker-2.0/test/models/test_domains.py +76 -0
- postmarker-2.0/test/models/test_emails.py +487 -0
- postmarker-2.0/test/models/test_message_streams.py +251 -0
- postmarker-2.0/test/models/test_messages.py +180 -0
- postmarker-2.0/test/models/test_senders.py +105 -0
- postmarker-2.0/test/models/test_server.py +13 -0
- postmarker-2.0/test/models/test_servers.py +158 -0
- postmarker-2.0/test/models/test_stats.py +130 -0
- postmarker-2.0/test/models/test_status.py +92 -0
- postmarker-2.0/test/models/test_suppressions.py +180 -0
- postmarker-2.0/test/models/test_templates.py +232 -0
- postmarker-2.0/test/models/test_triggers.py +58 -0
- postmarker-2.0/test/models/test_webhooks.py +103 -0
- postmarker-2.0/test/test_core.py +106 -0
- {postmarker-1.0 → postmarker-2.0}/test/test_exceptions.py +1 -1
- postmarker-2.0/test/test_pytest.py +18 -0
- {postmarker-1.0 → postmarker-2.0}/test/test_spamcheck.py +1 -1
- postmarker-2.0/test/tornado/__init__.py +0 -0
- postmarker-2.0/test/tornado/test_handlers.py +102 -0
- postmarker-2.0/test/typing_checks.py +24 -0
- postmarker-1.0/PKG-INFO +0 -147
- postmarker-1.0/README.rst +0 -115
- postmarker-1.0/pyproject.toml +0 -16
- postmarker-1.0/setup.cfg +0 -4
- postmarker-1.0/setup.py +0 -45
- postmarker-1.0/src/postmarker/__init__.py +0 -1
- postmarker-1.0/src/postmarker/django/__init__.py +0 -2
- postmarker-1.0/src/postmarker/models/base.py +0 -118
- postmarker-1.0/src/postmarker/models/domains.py +0 -71
- postmarker-1.0/src/postmarker/models/senders.py +0 -82
- postmarker-1.0/src/postmarker/models/templates.py +0 -85
- postmarker-1.0/src/postmarker/pytest.py +0 -20
- postmarker-1.0/src/postmarker.egg-info/PKG-INFO +0 -147
- postmarker-1.0/src/postmarker.egg-info/SOURCES.txt +0 -37
- postmarker-1.0/src/postmarker.egg-info/dependency_links.txt +0 -1
- postmarker-1.0/src/postmarker.egg-info/entry_points.txt +0 -3
- postmarker-1.0/src/postmarker.egg-info/requires.txt +0 -1
- postmarker-1.0/src/postmarker.egg-info/top_level.txt +0 -1
- postmarker-1.0/test/test_core.py +0 -70
- postmarker-1.0/test/test_pytest.py +0 -6
- {postmarker-1.0 → postmarker-2.0}/LICENSE +0 -0
- {postmarker-1.0 → postmarker-2.0}/src/postmarker/django/signals.py +0 -0
- {postmarker-1.0 → postmarker-2.0}/src/postmarker/models/__init__.py +0 -0
- {postmarker-1.0 → postmarker-2.0}/test/test_utils.py +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
env/
|
|
12
|
+
build/
|
|
13
|
+
develop-eggs/
|
|
14
|
+
dist/
|
|
15
|
+
downloads/
|
|
16
|
+
eggs/
|
|
17
|
+
.eggs/
|
|
18
|
+
lib/
|
|
19
|
+
lib64/
|
|
20
|
+
parts/
|
|
21
|
+
sdist/
|
|
22
|
+
var/
|
|
23
|
+
*.egg-info/
|
|
24
|
+
.installed.cfg
|
|
25
|
+
*.egg
|
|
26
|
+
|
|
27
|
+
# PyInstaller
|
|
28
|
+
# Usually these files are written by a python script from a template
|
|
29
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
30
|
+
*.manifest
|
|
31
|
+
*.spec
|
|
32
|
+
|
|
33
|
+
# Installer logs
|
|
34
|
+
pip-log.txt
|
|
35
|
+
pip-delete-this-directory.txt
|
|
36
|
+
|
|
37
|
+
# Unit test / coverage reports
|
|
38
|
+
htmlcov/
|
|
39
|
+
.tox/
|
|
40
|
+
.coverage
|
|
41
|
+
.coverage.*
|
|
42
|
+
.cache
|
|
43
|
+
nosetests.xml
|
|
44
|
+
coverage.xml
|
|
45
|
+
*,cover
|
|
46
|
+
.hypothesis/
|
|
47
|
+
|
|
48
|
+
# Translations
|
|
49
|
+
*.mo
|
|
50
|
+
*.pot
|
|
51
|
+
|
|
52
|
+
# Django stuff:
|
|
53
|
+
*.log
|
|
54
|
+
local_settings.py
|
|
55
|
+
|
|
56
|
+
# Flask stuff:
|
|
57
|
+
instance/
|
|
58
|
+
.webassets-cache
|
|
59
|
+
|
|
60
|
+
# Scrapy stuff:
|
|
61
|
+
.scrapy
|
|
62
|
+
|
|
63
|
+
# Documentation build
|
|
64
|
+
site/
|
|
65
|
+
|
|
66
|
+
# PyBuilder
|
|
67
|
+
target/
|
|
68
|
+
|
|
69
|
+
# IPython Notebook
|
|
70
|
+
.ipynb_checkpoints
|
|
71
|
+
|
|
72
|
+
# pyenv
|
|
73
|
+
.python-version
|
|
74
|
+
|
|
75
|
+
# celery beat schedule file
|
|
76
|
+
celerybeat-schedule
|
|
77
|
+
|
|
78
|
+
# dotenv
|
|
79
|
+
.env
|
|
80
|
+
|
|
81
|
+
# virtualenv
|
|
82
|
+
venv/
|
|
83
|
+
ENV/
|
|
84
|
+
|
|
85
|
+
# Spyder project settings
|
|
86
|
+
.spyderproject
|
|
87
|
+
|
|
88
|
+
# Rope project settings
|
|
89
|
+
.ropeproject
|
|
90
|
+
|
|
91
|
+
.idea/
|
|
92
|
+
.vscode/
|
|
93
|
+
|
|
94
|
+
.worktrees/
|
postmarker-2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: postmarker
|
|
3
|
+
Version: 2.0
|
|
4
|
+
Summary: Python client library for Postmark API
|
|
5
|
+
Project-URL: Documentation, https://postmarker.readthedocs.io/en/stable/
|
|
6
|
+
Project-URL: Changelog, https://github.com/Stranger6667/postmarker/blob/master/CHANGELOG.md
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/Stranger6667/postmarker/issues
|
|
8
|
+
Project-URL: Funding, https://github.com/sponsors/Stranger6667
|
|
9
|
+
Project-URL: Source Code, https://github.com/Stranger6667/postmarker
|
|
10
|
+
Author-email: Dmitry Dygalo <dmitry@dygalo.dev>
|
|
11
|
+
Maintainer-email: Dmitry Dygalo <dmitry@dygalo.dev>
|
|
12
|
+
License-Expression: MIT
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Keywords: api,client,django,email,email backend,postmark,postmarkapp,transactional email
|
|
15
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
16
|
+
Classifier: Environment :: Console
|
|
17
|
+
Classifier: Environment :: Web Environment
|
|
18
|
+
Classifier: Framework :: Django
|
|
19
|
+
Classifier: Framework :: Django :: 5.2
|
|
20
|
+
Classifier: Framework :: Django :: 6.0
|
|
21
|
+
Classifier: Framework :: Django :: 6.1
|
|
22
|
+
Classifier: Framework :: Pytest
|
|
23
|
+
Classifier: Intended Audience :: Developers
|
|
24
|
+
Classifier: Operating System :: OS Independent
|
|
25
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
32
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
33
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
34
|
+
Classifier: Topic :: Communications :: Email
|
|
35
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
36
|
+
Classifier: Typing :: Typed
|
|
37
|
+
Requires-Python: >=3.10
|
|
38
|
+
Requires-Dist: requests>=2.20.0
|
|
39
|
+
Provides-Extra: docs
|
|
40
|
+
Requires-Dist: mkdocstrings[python]; extra == 'docs'
|
|
41
|
+
Requires-Dist: pymdown-extensions; extra == 'docs'
|
|
42
|
+
Requires-Dist: ruff; extra == 'docs'
|
|
43
|
+
Requires-Dist: zensical<0.2,>=0.1.1; extra == 'docs'
|
|
44
|
+
Provides-Extra: tests
|
|
45
|
+
Requires-Dist: coverage[toml]>=7; extra == 'tests'
|
|
46
|
+
Requires-Dist: pytest-django; extra == 'tests'
|
|
47
|
+
Requires-Dist: pytest-recording>=0.14; extra == 'tests'
|
|
48
|
+
Requires-Dist: pytest>=8; extra == 'tests'
|
|
49
|
+
Requires-Dist: tornado; extra == 'tests'
|
|
50
|
+
Description-Content-Type: text/markdown
|
|
51
|
+
|
|
52
|
+
# Postmarker
|
|
53
|
+
|
|
54
|
+
[](https://github.com/Stranger6667/postmarker/actions/workflows/build.yml) [](https://codecov.io/gh/Stranger6667/postmarker) [](https://pypi.org/project/postmarker/) [](https://pypi.org/project/postmarker/) [](https://postmarker.readthedocs.io/en/stable/) [](https://opensource.org/licenses/MIT)
|
|
55
|
+
|
|
56
|
+
Python client library for [Postmark API](https://postmarkapp.com/developer).
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
Postmarker can be obtained with `pip`:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
pip install postmarker
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Usage example
|
|
67
|
+
|
|
68
|
+
**NOTE**:
|
|
69
|
+
|
|
70
|
+
The attributes of all classes are provided **as is**, without transformation to snake case. We don't want to introduce new names for existing entities.
|
|
71
|
+
|
|
72
|
+
Send single email:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
>>> from postmarker.core import PostmarkClient
|
|
76
|
+
>>> postmark = PostmarkClient(server_token='API_TOKEN')
|
|
77
|
+
>>> postmark.emails.send(
|
|
78
|
+
From='sender@example.com',
|
|
79
|
+
To='receiver@example.com',
|
|
80
|
+
Subject='Postmark test',
|
|
81
|
+
HtmlBody='<strong>Hello</strong> dear Postmark user.'
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Send batch:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
>>> postmark.emails.send_batch(
|
|
89
|
+
{
|
|
90
|
+
'From': 'sender@example.com',
|
|
91
|
+
'To': 'receiver@example.com',
|
|
92
|
+
'Subject': 'Postmark test',
|
|
93
|
+
'HtmlBody': '<strong>Hello</strong> dear Postmark user.',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
'From': 'sender2@example.com',
|
|
97
|
+
'To': 'receiver2@example.com',
|
|
98
|
+
'Subject': 'Postmark test 2',
|
|
99
|
+
'HtmlBody': '<strong>Hello</strong> dear Postmark user.',
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Setup an email:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
>>> email = postmark.emails.Email(
|
|
108
|
+
From='sender@example.com',
|
|
109
|
+
To='receiver@example.com',
|
|
110
|
+
Subject='Postmark test',
|
|
111
|
+
HtmlBody='<strong>Hello</strong> dear Postmark user.'
|
|
112
|
+
)
|
|
113
|
+
>>> email['X-Accept-Language'] = 'en-us, en'
|
|
114
|
+
>>> email.attach('/home/user/readme.txt')
|
|
115
|
+
>>> email.attach_binary(content=b'content', filename='readme.txt')
|
|
116
|
+
>>> email.send()
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
There are a lot of features available. Check it out in our documentation! Here's just a few of them:
|
|
120
|
+
|
|
121
|
+
- Support for sending Python email instances.
|
|
122
|
+
- Server APIs: Bounces, Messages, Message Streams, Suppressions, Templates with layouts, Server, Stats and Triggers.
|
|
123
|
+
- Account APIs: Domains, Sender signatures, Servers and pushing templates between servers.
|
|
124
|
+
- Status API.
|
|
125
|
+
- Django email backend.
|
|
126
|
+
- Tornado helper.
|
|
127
|
+
- Spam check API.
|
|
128
|
+
- Wrappers for Bounce, Inbound, Open and Delivery webhooks.
|
|
129
|
+
- `pytest` fixtures to mock Postmark API in tests.
|
|
130
|
+
- Type annotations, with a `py.typed` marker.
|
|
131
|
+
|
|
132
|
+
## Documentation
|
|
133
|
+
|
|
134
|
+
You can view the documentation online at:
|
|
135
|
+
|
|
136
|
+
- <https://postmarker.readthedocs.io/en/stable/>
|
|
137
|
+
|
|
138
|
+
Or you can look at the `docs/` directory in the repository.
|
|
139
|
+
|
|
140
|
+
## Python support
|
|
141
|
+
|
|
142
|
+
Postmarker supports CPython 3.10 - 3.15 and PyPy 3.11. The Django email backend supports Django 5.2, 6.0 and 6.1.
|
|
143
|
+
|
|
144
|
+
## Thanks
|
|
145
|
+
|
|
146
|
+
Many thanks to [Shmele](https://github.com/butorov) and [lobziik](https://github.com/lobziik) for their reviews and advice :)
|
postmarker-2.0/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Postmarker
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Stranger6667/postmarker/actions/workflows/build.yml) [](https://codecov.io/gh/Stranger6667/postmarker) [](https://pypi.org/project/postmarker/) [](https://pypi.org/project/postmarker/) [](https://postmarker.readthedocs.io/en/stable/) [](https://opensource.org/licenses/MIT)
|
|
4
|
+
|
|
5
|
+
Python client library for [Postmark API](https://postmarkapp.com/developer).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Postmarker can be obtained with `pip`:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
pip install postmarker
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage example
|
|
16
|
+
|
|
17
|
+
**NOTE**:
|
|
18
|
+
|
|
19
|
+
The attributes of all classes are provided **as is**, without transformation to snake case. We don't want to introduce new names for existing entities.
|
|
20
|
+
|
|
21
|
+
Send single email:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
>>> from postmarker.core import PostmarkClient
|
|
25
|
+
>>> postmark = PostmarkClient(server_token='API_TOKEN')
|
|
26
|
+
>>> postmark.emails.send(
|
|
27
|
+
From='sender@example.com',
|
|
28
|
+
To='receiver@example.com',
|
|
29
|
+
Subject='Postmark test',
|
|
30
|
+
HtmlBody='<strong>Hello</strong> dear Postmark user.'
|
|
31
|
+
)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Send batch:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
>>> postmark.emails.send_batch(
|
|
38
|
+
{
|
|
39
|
+
'From': 'sender@example.com',
|
|
40
|
+
'To': 'receiver@example.com',
|
|
41
|
+
'Subject': 'Postmark test',
|
|
42
|
+
'HtmlBody': '<strong>Hello</strong> dear Postmark user.',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
'From': 'sender2@example.com',
|
|
46
|
+
'To': 'receiver2@example.com',
|
|
47
|
+
'Subject': 'Postmark test 2',
|
|
48
|
+
'HtmlBody': '<strong>Hello</strong> dear Postmark user.',
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Setup an email:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
>>> email = postmark.emails.Email(
|
|
57
|
+
From='sender@example.com',
|
|
58
|
+
To='receiver@example.com',
|
|
59
|
+
Subject='Postmark test',
|
|
60
|
+
HtmlBody='<strong>Hello</strong> dear Postmark user.'
|
|
61
|
+
)
|
|
62
|
+
>>> email['X-Accept-Language'] = 'en-us, en'
|
|
63
|
+
>>> email.attach('/home/user/readme.txt')
|
|
64
|
+
>>> email.attach_binary(content=b'content', filename='readme.txt')
|
|
65
|
+
>>> email.send()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
There are a lot of features available. Check it out in our documentation! Here's just a few of them:
|
|
69
|
+
|
|
70
|
+
- Support for sending Python email instances.
|
|
71
|
+
- Server APIs: Bounces, Messages, Message Streams, Suppressions, Templates with layouts, Server, Stats and Triggers.
|
|
72
|
+
- Account APIs: Domains, Sender signatures, Servers and pushing templates between servers.
|
|
73
|
+
- Status API.
|
|
74
|
+
- Django email backend.
|
|
75
|
+
- Tornado helper.
|
|
76
|
+
- Spam check API.
|
|
77
|
+
- Wrappers for Bounce, Inbound, Open and Delivery webhooks.
|
|
78
|
+
- `pytest` fixtures to mock Postmark API in tests.
|
|
79
|
+
- Type annotations, with a `py.typed` marker.
|
|
80
|
+
|
|
81
|
+
## Documentation
|
|
82
|
+
|
|
83
|
+
You can view the documentation online at:
|
|
84
|
+
|
|
85
|
+
- <https://postmarker.readthedocs.io/en/stable/>
|
|
86
|
+
|
|
87
|
+
Or you can look at the `docs/` directory in the repository.
|
|
88
|
+
|
|
89
|
+
## Python support
|
|
90
|
+
|
|
91
|
+
Postmarker supports CPython 3.10 - 3.15 and PyPy 3.11. The Django email backend supports Django 5.2, 6.0 and 6.1.
|
|
92
|
+
|
|
93
|
+
## Thanks
|
|
94
|
+
|
|
95
|
+
Many thanks to [Shmele](https://github.com/butorov) and [lobziik](https://github.com/lobziik) for their reviews and advice :)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "postmarker"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Python client library for Postmark API"
|
|
9
|
+
keywords = ["postmark", "postmarkapp", "api", "client", "email", "transactional email", "django", "email backend"]
|
|
10
|
+
authors = [{ name = "Dmitry Dygalo", email = "dmitry@dygalo.dev" }]
|
|
11
|
+
maintainers = [{ name = "Dmitry Dygalo", email = "dmitry@dygalo.dev" }]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = "MIT"
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 5 - Production/Stable",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Environment :: Web Environment",
|
|
19
|
+
"Framework :: Django",
|
|
20
|
+
"Framework :: Django :: 5.2",
|
|
21
|
+
"Framework :: Django :: 6.0",
|
|
22
|
+
"Framework :: Django :: 6.1",
|
|
23
|
+
"Framework :: Pytest",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
27
|
+
"Programming Language :: Python :: 3.10",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Programming Language :: Python :: 3.13",
|
|
31
|
+
"Programming Language :: Python :: 3.14",
|
|
32
|
+
"Programming Language :: Python :: 3.15",
|
|
33
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
34
|
+
"Programming Language :: Python :: Implementation :: PyPy",
|
|
35
|
+
"Topic :: Communications :: Email",
|
|
36
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
37
|
+
"Typing :: Typed",
|
|
38
|
+
]
|
|
39
|
+
dependencies = ["requests>=2.20.0"]
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
tests = ["coverage[toml]>=7", "pytest>=8", "pytest-django", "pytest-recording>=0.14", "tornado"]
|
|
43
|
+
docs = ["mkdocstrings[python]", "pymdown-extensions", "ruff", "zensical>=0.1.1,<0.2"]
|
|
44
|
+
|
|
45
|
+
[project.entry-points.pytest11]
|
|
46
|
+
postmark = "postmarker.pytest"
|
|
47
|
+
|
|
48
|
+
[project.urls]
|
|
49
|
+
Documentation = "https://postmarker.readthedocs.io/en/stable/"
|
|
50
|
+
Changelog = "https://github.com/Stranger6667/postmarker/blob/master/CHANGELOG.md"
|
|
51
|
+
"Bug Tracker" = "https://github.com/Stranger6667/postmarker/issues"
|
|
52
|
+
Funding = "https://github.com/sponsors/Stranger6667"
|
|
53
|
+
"Source Code" = "https://github.com/Stranger6667/postmarker"
|
|
54
|
+
|
|
55
|
+
[tool.hatch.version]
|
|
56
|
+
path = "src/postmarker/__init__.py"
|
|
57
|
+
|
|
58
|
+
[tool.hatch.build.targets.sdist]
|
|
59
|
+
include = ["/src", "/test"]
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
addopts = ["-ra", "--strict-markers", "--strict-config", "--block-network", "--allowed-hosts=127.0.0.1,localhost"]
|
|
63
|
+
xfail_strict = true
|
|
64
|
+
testpaths = ["test"]
|
|
65
|
+
pythonpath = ["."]
|
|
66
|
+
DJANGO_SETTINGS_MODULE = "test.django.settings"
|
|
67
|
+
|
|
68
|
+
[tool.coverage.run]
|
|
69
|
+
parallel = true
|
|
70
|
+
branch = true
|
|
71
|
+
source_pkgs = ["postmarker"]
|
|
72
|
+
|
|
73
|
+
[tool.coverage.paths]
|
|
74
|
+
source = ["src", ".tox/*/site-packages", ".tox/*/lib/*/site-packages", ".tox/*/Lib/site-packages"]
|
|
75
|
+
|
|
76
|
+
[tool.coverage.report]
|
|
77
|
+
show_missing = true
|
|
78
|
+
precision = 2
|
|
79
|
+
exclude_lines = ["raise NotImplementedError"]
|
|
80
|
+
|
|
81
|
+
[tool.ruff]
|
|
82
|
+
line-length = 120
|
|
83
|
+
target-version = "py310"
|
|
84
|
+
|
|
85
|
+
[tool.ruff.lint]
|
|
86
|
+
select = [
|
|
87
|
+
"E", # pycodestyle errors
|
|
88
|
+
"W", # pycodestyle warnings
|
|
89
|
+
"F", # pyflakes
|
|
90
|
+
"C", # flake8-comprehensions
|
|
91
|
+
"B", # flake8-bugbear
|
|
92
|
+
"D", # pydocstyle
|
|
93
|
+
"T201", # flake8-print
|
|
94
|
+
"I", # isort
|
|
95
|
+
"FURB", # refurb
|
|
96
|
+
"PGH003", # blanket-type-ignore
|
|
97
|
+
"UP", # pyupgrade
|
|
98
|
+
"SIM", # flake8-simplify
|
|
99
|
+
]
|
|
100
|
+
ignore = [
|
|
101
|
+
"E501", # Line too long
|
|
102
|
+
"B008", # Do not perform function calls in argument defaults
|
|
103
|
+
"C901", # Too complex
|
|
104
|
+
"D100", # Missing docstring in public module
|
|
105
|
+
"D101", # Missing docstring in public class
|
|
106
|
+
"D102", # Missing docstring in public method
|
|
107
|
+
"D103", # Missing docstring in public function
|
|
108
|
+
"D104", # Missing docstring in public package
|
|
109
|
+
"D105", # Missing docstring in magic method
|
|
110
|
+
"D106", # Missing docstring in public nested class
|
|
111
|
+
"D107", # Missing docstring in `__init__`
|
|
112
|
+
"D202", # No blank lines allowed after function docstring
|
|
113
|
+
"D203", # One blank line before class
|
|
114
|
+
"D213", # Multiline summary second line
|
|
115
|
+
"D401", # Imperative mood
|
|
116
|
+
"D407", # Missing dashed underline after section
|
|
117
|
+
"SIM105", # contextlib.suppress is less performant in many cases
|
|
118
|
+
"SIM108", # ternary expressions are not always readable
|
|
119
|
+
"SIM910", # delete default arguments in .get() in some cases
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
[tool.ruff.lint.isort]
|
|
123
|
+
known-first-party = ["postmarker"]
|
|
124
|
+
|
|
125
|
+
[tool.typos.default]
|
|
126
|
+
# Verbatim Postmark spam check API message
|
|
127
|
+
extend-ignore-re = ["SpamAssassin error occured"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.0"
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import sys
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import Any, TextIO
|
|
2
6
|
from urllib.parse import urljoin
|
|
3
7
|
|
|
4
8
|
import requests
|
|
@@ -6,52 +10,74 @@ import requests
|
|
|
6
10
|
from . import __version__
|
|
7
11
|
from .exceptions import ClientError, SpamAssassinError
|
|
8
12
|
from .logging import get_logger
|
|
13
|
+
from .models.base import ModelManager
|
|
9
14
|
from .models.bounces import BounceManager
|
|
10
15
|
from .models.domains import DomainsManager
|
|
11
16
|
from .models.emails import EmailManager
|
|
17
|
+
from .models.message_streams import MessageStreamsManager
|
|
12
18
|
from .models.messages import MessageManager
|
|
13
19
|
from .models.senders import SenderSignaturesManager
|
|
14
20
|
from .models.server import ServerManager
|
|
21
|
+
from .models.servers import ServersManager
|
|
15
22
|
from .models.stats import StatsManager
|
|
16
23
|
from .models.status import StatusManager
|
|
24
|
+
from .models.suppressions import SuppressionsManager
|
|
17
25
|
from .models.templates import TemplateManager
|
|
18
26
|
from .models.triggers import TriggersManager
|
|
19
27
|
from .utils import get_args
|
|
20
28
|
|
|
21
29
|
DEFAULT_API = "https://api.postmarkapp.com/"
|
|
22
30
|
STATUS_API = "https://status.postmarkapp.com/api/1.0/"
|
|
23
|
-
SPAMCHECK_API = "
|
|
24
|
-
USER_AGENT = "Postmarker
|
|
31
|
+
SPAMCHECK_API = "https://spamcheck.postmarkapp.com/filter/"
|
|
32
|
+
USER_AGENT = f"Postmarker/{__version__}"
|
|
25
33
|
TEST_TOKEN = "POSTMARK_API_TEST"
|
|
26
34
|
|
|
27
35
|
|
|
28
36
|
class PostmarkClient:
|
|
29
37
|
"""Basic class for API clients. Provides basic functionality to make requests."""
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
bounces: BounceManager
|
|
40
|
+
domains: DomainsManager
|
|
41
|
+
emails: EmailManager
|
|
42
|
+
messages: MessageManager
|
|
43
|
+
message_streams: MessageStreamsManager
|
|
44
|
+
senders: SenderSignaturesManager
|
|
45
|
+
server: ServerManager
|
|
46
|
+
servers: ServersManager
|
|
47
|
+
stats: StatsManager
|
|
48
|
+
status: StatusManager
|
|
49
|
+
suppressions: SuppressionsManager
|
|
50
|
+
templates: TemplateManager
|
|
51
|
+
triggers: TriggersManager
|
|
52
|
+
_session: requests.Session
|
|
53
|
+
|
|
54
|
+
_managers: tuple[type[ModelManager[Any]], ...] = (
|
|
32
55
|
BounceManager,
|
|
33
56
|
DomainsManager,
|
|
34
57
|
EmailManager,
|
|
35
58
|
MessageManager,
|
|
59
|
+
MessageStreamsManager,
|
|
36
60
|
SenderSignaturesManager,
|
|
37
61
|
ServerManager,
|
|
62
|
+
ServersManager,
|
|
38
63
|
StatsManager,
|
|
39
64
|
StatusManager,
|
|
65
|
+
SuppressionsManager,
|
|
40
66
|
TemplateManager,
|
|
41
67
|
TriggersManager,
|
|
42
68
|
)
|
|
43
69
|
|
|
44
70
|
def __init__(
|
|
45
71
|
self,
|
|
46
|
-
server_token=None,
|
|
47
|
-
account_token=None,
|
|
48
|
-
verbosity=0,
|
|
49
|
-
max_retries=0,
|
|
50
|
-
timeout=None,
|
|
51
|
-
logs_stream=sys.stdout,
|
|
52
|
-
root_api_url=DEFAULT_API,
|
|
53
|
-
):
|
|
54
|
-
assert server_token, "You have to provide
|
|
72
|
+
server_token: str | None = None,
|
|
73
|
+
account_token: str | None = None,
|
|
74
|
+
verbosity: int = 0,
|
|
75
|
+
max_retries: int = 0,
|
|
76
|
+
timeout: float | tuple[float, float] | None = None,
|
|
77
|
+
logs_stream: TextIO = sys.stdout,
|
|
78
|
+
root_api_url: str = DEFAULT_API,
|
|
79
|
+
) -> None:
|
|
80
|
+
assert server_token or account_token, "You have to provide server_token or account_token to use Postmark API"
|
|
55
81
|
self.server_token = server_token
|
|
56
82
|
self.account_token = account_token
|
|
57
83
|
self.max_retries = max_retries
|
|
@@ -61,7 +87,9 @@ class PostmarkClient:
|
|
|
61
87
|
self._setup_managers()
|
|
62
88
|
|
|
63
89
|
@classmethod
|
|
64
|
-
def from_config(
|
|
90
|
+
def from_config(
|
|
91
|
+
cls, config: Mapping[str, Any], prefix: str = "postmark_", is_uppercase: bool = False
|
|
92
|
+
) -> PostmarkClient:
|
|
65
93
|
"""Helper method for instantiating PostmarkClient from dict-like objects."""
|
|
66
94
|
kwargs = {}
|
|
67
95
|
for arg in get_args(cls):
|
|
@@ -74,10 +102,10 @@ class PostmarkClient:
|
|
|
74
102
|
kwargs[arg] = config[key]
|
|
75
103
|
return cls(**kwargs)
|
|
76
104
|
|
|
77
|
-
def __repr__(self):
|
|
105
|
+
def __repr__(self) -> str:
|
|
78
106
|
return f"<{self.__class__.__name__}: {self.server_token}>"
|
|
79
107
|
|
|
80
|
-
def _setup_managers(self):
|
|
108
|
+
def _setup_managers(self) -> None:
|
|
81
109
|
"""Allows to access manager by model name.
|
|
82
110
|
|
|
83
111
|
>>> postmark = PostmarkClient(server_token='TEST')
|
|
@@ -89,15 +117,15 @@ class PostmarkClient:
|
|
|
89
117
|
setattr(self, instance.name, instance)
|
|
90
118
|
|
|
91
119
|
@property
|
|
92
|
-
def session(self):
|
|
120
|
+
def session(self) -> requests.Session:
|
|
93
121
|
if not hasattr(self, "_session"):
|
|
94
|
-
self._session = requests.Session()
|
|
122
|
+
self._session = requests.Session()
|
|
95
123
|
adapter = requests.adapters.HTTPAdapter(max_retries=self.max_retries)
|
|
96
124
|
self._session.mount("http://", adapter)
|
|
97
125
|
self._session.mount("https://", adapter)
|
|
98
126
|
return self._session
|
|
99
127
|
|
|
100
|
-
def call(self, method, endpoint, token_type="server", data=None, **kwargs):
|
|
128
|
+
def call(self, method: str, endpoint: str, token_type: str = "server", data: Any = None, **kwargs: Any) -> Any:
|
|
101
129
|
if token_type == "account":
|
|
102
130
|
header = "X-Postmark-Account-Token"
|
|
103
131
|
token = self.account_token
|
|
@@ -106,18 +134,26 @@ class PostmarkClient:
|
|
|
106
134
|
token = self.server_token
|
|
107
135
|
return self._call(method, self.root_api_url, endpoint, data, {header: token}, **kwargs)
|
|
108
136
|
|
|
109
|
-
def call_status(self, endpoint):
|
|
137
|
+
def call_status(self, endpoint: str) -> Any:
|
|
110
138
|
return self._call("GET", STATUS_API, endpoint)
|
|
111
139
|
|
|
112
|
-
def spamcheck(self, dump, options="long"):
|
|
140
|
+
def spamcheck(self, dump: str, options: str = "long") -> Any:
|
|
113
141
|
data = {"email": dump, "options": options}
|
|
114
142
|
response = self._call("POST", SPAMCHECK_API, "", data)
|
|
115
143
|
if not response["success"]:
|
|
116
144
|
raise SpamAssassinError(response["message"])
|
|
117
145
|
return response
|
|
118
146
|
|
|
119
|
-
def _call(
|
|
120
|
-
|
|
147
|
+
def _call(
|
|
148
|
+
self,
|
|
149
|
+
method: str,
|
|
150
|
+
root: str,
|
|
151
|
+
endpoint: str,
|
|
152
|
+
data: Any = None,
|
|
153
|
+
headers: dict[str, str | None] | None = None,
|
|
154
|
+
**kwargs: Any,
|
|
155
|
+
) -> Any:
|
|
156
|
+
default_headers: dict[str, str | None] = {"Accept": "application/json", "User-Agent": USER_AGENT}
|
|
121
157
|
if headers:
|
|
122
158
|
default_headers.update(headers)
|
|
123
159
|
url = urljoin(root, endpoint)
|
|
@@ -134,7 +170,7 @@ class PostmarkClient:
|
|
|
134
170
|
self.check_response(response)
|
|
135
171
|
return response.json()
|
|
136
172
|
|
|
137
|
-
def check_response(self, response):
|
|
173
|
+
def check_response(self, response: requests.Response) -> None:
|
|
138
174
|
try:
|
|
139
175
|
response.raise_for_status()
|
|
140
176
|
except requests.HTTPError as http_error:
|
|
@@ -145,5 +181,5 @@ class PostmarkClient:
|
|
|
145
181
|
message = self.format_exception_message(data)
|
|
146
182
|
raise ClientError(message, error_code=data["ErrorCode"]) from http_error
|
|
147
183
|
|
|
148
|
-
def format_exception_message(self, data):
|
|
184
|
+
def format_exception_message(self, data: dict[str, Any]) -> str:
|
|
149
185
|
return "[{}] {}".format(data["ErrorCode"], data["Message"])
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .backend import EmailBackend, PostmarkEmailMessage, PostmarkEmailMixin, PostmarkEmailMultiAlternatives
|
|
2
|
+
from .signals import on_exception, post_send, pre_send
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"EmailBackend",
|
|
6
|
+
"PostmarkEmailMessage",
|
|
7
|
+
"PostmarkEmailMixin",
|
|
8
|
+
"PostmarkEmailMultiAlternatives",
|
|
9
|
+
"on_exception",
|
|
10
|
+
"post_send",
|
|
11
|
+
"pre_send",
|
|
12
|
+
]
|