postmarker 1.0__tar.gz → 2.1__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.1/.gitignore +94 -0
- postmarker-2.1/PKG-INFO +203 -0
- postmarker-2.1/README.md +149 -0
- postmarker-2.1/pyproject.toml +129 -0
- postmarker-2.1/src/postmarker/__init__.py +1 -0
- {postmarker-1.0 → postmarker-2.1}/src/postmarker/core.py +80 -24
- postmarker-2.1/src/postmarker/django/__init__.py +12 -0
- {postmarker-1.0 → postmarker-2.1}/src/postmarker/django/backend.py +36 -19
- {postmarker-1.0 → postmarker-2.1}/src/postmarker/exceptions.py +6 -1
- {postmarker-1.0 → postmarker-2.1}/src/postmarker/logging.py +4 -1
- postmarker-2.1/src/postmarker/models/base.py +134 -0
- {postmarker-1.0 → postmarker-2.1}/src/postmarker/models/bounces.py +43 -22
- postmarker-2.1/src/postmarker/models/domains.py +75 -0
- {postmarker-1.0 → postmarker-2.1}/src/postmarker/models/emails.py +193 -142
- postmarker-2.1/src/postmarker/models/message_streams.py +95 -0
- {postmarker-1.0 → postmarker-2.1}/src/postmarker/models/messages.py +153 -76
- postmarker-2.1/src/postmarker/models/senders.py +106 -0
- {postmarker-1.0 → postmarker-2.1}/src/postmarker/models/server.py +9 -5
- postmarker-2.1/src/postmarker/models/servers.py +55 -0
- {postmarker-1.0 → postmarker-2.1}/src/postmarker/models/stats.py +19 -15
- {postmarker-1.0 → postmarker-2.1}/src/postmarker/models/status.py +22 -15
- postmarker-2.1/src/postmarker/models/suppressions.py +95 -0
- postmarker-2.1/src/postmarker/models/templates.py +155 -0
- {postmarker-1.0 → postmarker-2.1}/src/postmarker/models/triggers.py +27 -22
- postmarker-2.1/src/postmarker/py.typed +0 -0
- postmarker-2.1/src/postmarker/pytest.py +34 -0
- {postmarker-1.0 → postmarker-2.1}/src/postmarker/tornado.py +15 -4
- {postmarker-1.0 → postmarker-2.1}/src/postmarker/utils.py +9 -3
- postmarker-2.1/test/__init__.py +0 -0
- postmarker-2.1/test/cassettes/test_exceptions/test_raise.yaml +37 -0
- postmarker-2.1/test/cassettes/test_spamcheck/test_spamcheck_invalid.yaml +41 -0
- postmarker-2.1/test/cassettes/test_spamcheck/test_spamcheck_valid.yaml +60 -0
- postmarker-2.1/test/conftest.py +239 -0
- postmarker-2.1/test/django/__init__.py +0 -0
- postmarker-2.1/test/django/conftest.py +11 -0
- postmarker-2.1/test/django/settings.py +4 -0
- postmarker-2.1/test/django/test_backend.py +573 -0
- postmarker-2.1/test/django/test_mixins.py +92 -0
- postmarker-2.1/test/models/__init__.py +0 -0
- postmarker-2.1/test/models/attachments/readme.txt +1 -0
- postmarker-2.1/test/models/attachments/report.blabla +1 -0
- postmarker-2.1/test/models/cassettes/test_bounces/test_load_all_bounces_multiple_calls[2-1-2].yaml +77 -0
- postmarker-2.1/test/models/cassettes/test_bounces/test_load_all_bounces_multiple_calls[2-50-1].yaml +42 -0
- postmarker-2.1/test/models/cassettes/test_bounces/test_load_all_bounces_multiple_calls[4-1-2].yaml +77 -0
- postmarker-2.1/test/models/cassettes/test_bounces/test_load_all_bounces_multiple_calls[None-1-2].yaml +77 -0
- postmarker-2.1/test/models/cassettes/test_bounces/test_manager_all.yaml +42 -0
- postmarker-2.1/test/models/cassettes/test_bounces/test_manager_deliverystats.yaml +38 -0
- postmarker-2.1/test/models/cassettes/test_bounces/test_manager_tags.yaml +36 -0
- postmarker-2.1/test/models/cassettes/test_bounces/test_message.yaml +78 -0
- postmarker-2.1/test/models/cassettes/test_bounces/test_model_activate.yaml +82 -0
- postmarker-2.1/test/models/cassettes/test_bounces/test_model_dump.yaml +75 -0
- postmarker-2.1/test/models/cassettes/test_bounces/test_model_get.yaml +41 -0
- postmarker-2.1/test/models/cassettes/test_bounces/test_model_repr.yaml +41 -0
- postmarker-2.1/test/models/cassettes/test_bounces/test_model_str.yaml +41 -0
- postmarker-2.1/test/models/cassettes/test_domains/test_manager_all.yaml +36 -0
- postmarker-2.1/test/models/cassettes/test_domains/test_manager_domain_methods.yaml +279 -0
- postmarker-2.1/test/models/cassettes/test_domains/test_model_get.yaml +74 -0
- postmarker-2.1/test/models/cassettes/test_domains/test_model_str.yaml +38 -0
- postmarker-2.1/test/models/cassettes/test_emails/test_simple_send_default.yaml +44 -0
- postmarker-2.1/test/models/cassettes/test_emails/test_simple_send_mime_text.yaml +43 -0
- postmarker-2.1/test/models/cassettes/test_emails/test_simple_send_minimum_mime.yaml +43 -0
- postmarker-2.1/test/models/cassettes/test_emails/test_simple_send_with_template.yaml +40 -0
- postmarker-2.1/test/models/cassettes/test_messages/test_inbound_message_bypass.yaml +39 -0
- postmarker-2.1/test/models/cassettes/test_messages/test_inbound_message_retry.yaml +39 -0
- postmarker-2.1/test/models/cassettes/test_messages/test_inbound_messages_all.yaml +36 -0
- postmarker-2.1/test/models/cassettes/test_messages/test_opens_model.yaml +37 -0
- postmarker-2.1/test/models/cassettes/test_messages/test_outbound_message.yaml +37 -0
- postmarker-2.1/test/models/cassettes/test_messages/test_outbound_message_get.yaml +75 -0
- postmarker-2.1/test/models/cassettes/test_messages/test_outbound_message_get_dump.yaml +71 -0
- postmarker-2.1/test/models/cassettes/test_messages/test_outbound_message_opens.yaml +71 -0
- postmarker-2.1/test/models/cassettes/test_senders/test_all.yaml +38 -0
- postmarker-2.1/test/models/cassettes/test_senders/test_all_methods.yaml +190 -0
- postmarker-2.1/test/models/cassettes/test_senders/test_edit.yaml +92 -0
- postmarker-2.1/test/models/cassettes/test_senders/test_get.yaml +88 -0
- postmarker-2.1/test/models/cassettes/test_server/test_edit.yaml +74 -0
- postmarker-2.1/test/models/cassettes/test_server/test_str.yaml +36 -0
- postmarker-2.1/test/models/cassettes/test_stats/test_methods[bounces-kwargs2-expected2].yaml +36 -0
- postmarker-2.1/test/models/cassettes/test_stats/test_methods[browserfamilies-kwargs10-expected10].yaml +36 -0
- postmarker-2.1/test/models/cassettes/test_stats/test_methods[clicks-kwargs9-expected9].yaml +36 -0
- postmarker-2.1/test/models/cassettes/test_stats/test_methods[clicks_platforms-kwargs11-expected11].yaml +36 -0
- postmarker-2.1/test/models/cassettes/test_stats/test_methods[emailclients-kwargs7-expected7].yaml +36 -0
- postmarker-2.1/test/models/cassettes/test_stats/test_methods[location-kwargs12-expected12].yaml +36 -0
- postmarker-2.1/test/models/cassettes/test_stats/test_methods[opens-kwargs5-expected5].yaml +36 -0
- postmarker-2.1/test/models/cassettes/test_stats/test_methods[opens_platforms-kwargs6-expected6].yaml +36 -0
- postmarker-2.1/test/models/cassettes/test_stats/test_methods[overview-kwargs0-expected0].yaml +36 -0
- postmarker-2.1/test/models/cassettes/test_stats/test_methods[readtimes-kwargs8-expected8].yaml +36 -0
- postmarker-2.1/test/models/cassettes/test_stats/test_methods[sends-kwargs1-expected1].yaml +36 -0
- postmarker-2.1/test/models/cassettes/test_stats/test_methods[spam-kwargs3-expected3].yaml +36 -0
- postmarker-2.1/test/models/cassettes/test_stats/test_methods[tracked-kwargs4-expected4].yaml +36 -0
- postmarker-2.1/test/models/cassettes/test_status/test_availability.yaml +40 -0
- postmarker-2.1/test/models/cassettes/test_status/test_delivery.yaml +40 -0
- postmarker-2.1/test/models/cassettes/test_status/test_get_incident.yaml +46 -0
- postmarker-2.1/test/models/cassettes/test_status/test_incidents.yaml +346 -0
- postmarker-2.1/test/models/cassettes/test_status/test_last_incident.yaml +46 -0
- postmarker-2.1/test/models/cassettes/test_status/test_services.yaml +43 -0
- postmarker-2.1/test/models/cassettes/test_status/test_status.yaml +40 -0
- postmarker-2.1/test/models/cassettes/test_templates/test_manager_all.yaml +36 -0
- postmarker-2.1/test/models/cassettes/test_templates/test_manager_create.yaml +77 -0
- postmarker-2.1/test/models/cassettes/test_templates/test_manager_edit.yaml +40 -0
- postmarker-2.1/test/models/cassettes/test_templates/test_manager_validate.yaml +41 -0
- postmarker-2.1/test/models/cassettes/test_templates/test_model_default.yaml +40 -0
- postmarker-2.1/test/models/cassettes/test_templates/test_model_edit.yaml +78 -0
- postmarker-2.1/test/models/cassettes/test_templates/test_model_get.yaml +78 -0
- postmarker-2.1/test/models/cassettes/test_templates/test_model_repr.yaml +40 -0
- postmarker-2.1/test/models/cassettes/test_triggers/test_inbound_rules_all.yaml +36 -0
- postmarker-2.1/test/models/cassettes/test_triggers/test_inbound_rules_create.yaml +40 -0
- postmarker-2.1/test/models/cassettes/test_triggers/test_inbound_rules_delete.yaml +72 -0
- postmarker-2.1/test/models/cassettes/test_triggers/test_tags_all.yaml +36 -0
- postmarker-2.1/test/models/cassettes/test_triggers/test_tags_create.yaml +40 -0
- postmarker-2.1/test/models/cassettes/test_triggers/test_tags_delete.yaml +72 -0
- postmarker-2.1/test/models/cassettes/test_triggers/test_tags_edit.yaml +78 -0
- postmarker-2.1/test/models/cassettes/test_triggers/test_tags_get.yaml +70 -0
- postmarker-2.1/test/models/test_base.py +72 -0
- postmarker-2.1/test/models/test_bounces.py +80 -0
- postmarker-2.1/test/models/test_clicks.py +244 -0
- postmarker-2.1/test/models/test_domains.py +76 -0
- postmarker-2.1/test/models/test_emails.py +579 -0
- postmarker-2.1/test/models/test_message_streams.py +251 -0
- postmarker-2.1/test/models/test_messages.py +180 -0
- postmarker-2.1/test/models/test_senders.py +105 -0
- postmarker-2.1/test/models/test_server.py +13 -0
- postmarker-2.1/test/models/test_servers.py +158 -0
- postmarker-2.1/test/models/test_stats.py +130 -0
- postmarker-2.1/test/models/test_status.py +92 -0
- postmarker-2.1/test/models/test_suppressions.py +180 -0
- postmarker-2.1/test/models/test_templates.py +232 -0
- postmarker-2.1/test/models/test_triggers.py +58 -0
- postmarker-2.1/test/models/test_webhooks.py +103 -0
- postmarker-2.1/test/test_core.py +106 -0
- {postmarker-1.0 → postmarker-2.1}/test/test_exceptions.py +1 -1
- postmarker-2.1/test/test_pytest.py +18 -0
- {postmarker-1.0 → postmarker-2.1}/test/test_spamcheck.py +1 -1
- postmarker-2.1/test/tornado/__init__.py +0 -0
- postmarker-2.1/test/tornado/test_handlers.py +102 -0
- postmarker-2.1/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.1}/LICENSE +0 -0
- {postmarker-1.0 → postmarker-2.1}/src/postmarker/django/signals.py +0 -0
- {postmarker-1.0 → postmarker-2.1}/src/postmarker/models/__init__.py +0 -0
- {postmarker-1.0 → postmarker-2.1}/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.1/PKG-INFO
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: postmarker
|
|
3
|
+
Version: 2.1
|
|
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: css-inline
|
|
40
|
+
Requires-Dist: css-inline>=0.24; extra == 'css-inline'
|
|
41
|
+
Provides-Extra: docs
|
|
42
|
+
Requires-Dist: mkdocstrings[python]; extra == 'docs'
|
|
43
|
+
Requires-Dist: pymdown-extensions; extra == 'docs'
|
|
44
|
+
Requires-Dist: ruff; extra == 'docs'
|
|
45
|
+
Requires-Dist: zensical<0.2,>=0.1.1; extra == 'docs'
|
|
46
|
+
Provides-Extra: tests
|
|
47
|
+
Requires-Dist: coverage[toml]>=7.2; extra == 'tests'
|
|
48
|
+
Requires-Dist: css-inline>=0.24; extra == 'tests'
|
|
49
|
+
Requires-Dist: pytest-django; extra == 'tests'
|
|
50
|
+
Requires-Dist: pytest-recording>=0.14; extra == 'tests'
|
|
51
|
+
Requires-Dist: pytest>=8; extra == 'tests'
|
|
52
|
+
Requires-Dist: tornado; extra == 'tests'
|
|
53
|
+
Description-Content-Type: text/markdown
|
|
54
|
+
|
|
55
|
+
# Postmarker
|
|
56
|
+
|
|
57
|
+
[](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)
|
|
58
|
+
|
|
59
|
+
Python client for the [Postmark](https://postmarkapp.com) email API.
|
|
60
|
+
|
|
61
|
+
Postmarker covers Postmark's API beyond sending: templates, bounces, suppressions, message streams, stats, servers and domains.
|
|
62
|
+
It also ships a Django email backend, Postmark webhook wrappers and a pytest fixture for your tests.
|
|
63
|
+
|
|
64
|
+
> [!NOTE]
|
|
65
|
+
> Postmarker keeps Postmark's field names: `From`, `HtmlBody`, `TemplateModel`, `MessageStream`.
|
|
66
|
+
> Copy any field from the [Postmark API docs](https://postmarkapp.com/developer) and pass it as is. There are no snake_case aliases.
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
uv pip install postmarker
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
To inline `<style>` CSS into HTML emails, install the `css-inline` extra and create the client with `inline_css=True`:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
uv pip install 'postmarker[css-inline]'
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Usage
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from postmarker.core import PostmarkClient
|
|
84
|
+
|
|
85
|
+
postmark = PostmarkClient(server_token="POSTMARK_API_TEST")
|
|
86
|
+
postmark.emails.send(
|
|
87
|
+
From="sender@example.com",
|
|
88
|
+
To="receiver@example.com",
|
|
89
|
+
Subject="Postmark test",
|
|
90
|
+
HtmlBody="<strong>Hello</strong> dear Postmark user.",
|
|
91
|
+
)
|
|
92
|
+
# {"ErrorCode": 0, "Message": "Test job accepted", "MessageID": "...", ...}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
With the `POSTMARK_API_TEST` token, Postmark validates the request without delivering it.
|
|
96
|
+
To send real emails, use the server token from your server's API Tokens tab in Postmark.
|
|
97
|
+
`From` must be a confirmed sender signature or an address on a verified domain.
|
|
98
|
+
|
|
99
|
+
Send with a template:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
postmark.emails.send_with_template(
|
|
103
|
+
TemplateAlias="welcome",
|
|
104
|
+
TemplateModel={"name": "Jane"},
|
|
105
|
+
From="sender@example.com",
|
|
106
|
+
To="receiver@example.com",
|
|
107
|
+
)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Send a batch. Postmarker splits it into requests of 500 messages, Postmark's limit:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
messages = [
|
|
114
|
+
{"From": "sender@example.com", "To": to, "Subject": "Hi", "TextBody": "Hello"}
|
|
115
|
+
for to in ("first@example.com", "second@example.com")
|
|
116
|
+
]
|
|
117
|
+
responses = postmark.emails.send_batch(*messages)
|
|
118
|
+
# Postmark accepts or rejects each message separately
|
|
119
|
+
failed = [response for response in responses if response["ErrorCode"] != 0]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Add headers and attachments:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
email = postmark.emails.Email(
|
|
126
|
+
From="sender@example.com",
|
|
127
|
+
To="receiver@example.com",
|
|
128
|
+
Subject="Report",
|
|
129
|
+
TextBody="See the attached report.",
|
|
130
|
+
)
|
|
131
|
+
email["X-Accept-Language"] = "en-us, en"
|
|
132
|
+
email.attach("/home/user/report.pdf")
|
|
133
|
+
email.attach_binary(content=b"content", filename="readme.txt")
|
|
134
|
+
email.send()
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
A rejected request raises `ClientError` with Postmark's [error code](https://postmarkapp.com/developer/api/overview#error-codes):
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from postmarker.exceptions import ClientError
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
postmark.emails.send(
|
|
144
|
+
From="sender@example.com",
|
|
145
|
+
To="receiver@example.com",
|
|
146
|
+
TextBody="Hello",
|
|
147
|
+
)
|
|
148
|
+
except ClientError as exc:
|
|
149
|
+
print(exc.error_code) # For example, 406 for an inactive recipient
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Django
|
|
153
|
+
|
|
154
|
+
Django 6.1+:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
MAILERS = {
|
|
158
|
+
"default": {
|
|
159
|
+
"BACKEND": "postmarker.django.EmailBackend",
|
|
160
|
+
"OPTIONS": {"token": "<YOUR POSTMARK SERVER TOKEN>"},
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Django 5.2 and 6.0:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
EMAIL_BACKEND = "postmarker.django.EmailBackend"
|
|
169
|
+
POSTMARK = {"TOKEN": "<YOUR POSTMARK SERVER TOKEN>"}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Django then sends `send_mail`, `EmailMessage` and emails from third-party apps through Postmark.
|
|
173
|
+
The [Django guide](https://postmarker.readthedocs.io/en/stable/django/) covers tags, metadata, message streams and signals.
|
|
174
|
+
|
|
175
|
+
## Testing
|
|
176
|
+
|
|
177
|
+
Postmarker ships a `postmark_request` pytest fixture.
|
|
178
|
+
It mocks requests to Postmark and records what your code sent, through `PostmarkClient` or the Django backend:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
def test_welcome_email(postmark_request):
|
|
182
|
+
send_welcome_email("jane@example.com") # Your code that sends an email
|
|
183
|
+
assert postmark_request.call_args.kwargs["json"]["To"] == "jane@example.com"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Features
|
|
187
|
+
|
|
188
|
+
- Server APIs: Emails, Templates with layouts, Bounces, Messages (inbound, outbound, opens and clicks), Message Streams, Suppressions, Server, Stats and Triggers.
|
|
189
|
+
- Account APIs: Domains, Sender signatures, Servers and pushing templates between servers.
|
|
190
|
+
- Status and Spam check APIs.
|
|
191
|
+
- Webhook wrappers: Inbound, Delivery, Open, Click, Bounce, Spam complaint and Subscription change.
|
|
192
|
+
- Sending `email.message.Message` instances.
|
|
193
|
+
- CSS inlining via [css-inline](https://github.com/Stranger6667/css-inline).
|
|
194
|
+
- Django email backend and Tornado helper.
|
|
195
|
+
- Type annotations, with a `py.typed` marker.
|
|
196
|
+
|
|
197
|
+
## Documentation
|
|
198
|
+
|
|
199
|
+
<https://postmarker.readthedocs.io/en/stable/>
|
|
200
|
+
|
|
201
|
+
## Python support
|
|
202
|
+
|
|
203
|
+
Postmarker supports CPython 3.10 to 3.15 and PyPy 3.11. The Django email backend supports Django 5.2, 6.0 and 6.1.
|
postmarker-2.1/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
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 for the [Postmark](https://postmarkapp.com) email API.
|
|
6
|
+
|
|
7
|
+
Postmarker covers Postmark's API beyond sending: templates, bounces, suppressions, message streams, stats, servers and domains.
|
|
8
|
+
It also ships a Django email backend, Postmark webhook wrappers and a pytest fixture for your tests.
|
|
9
|
+
|
|
10
|
+
> [!NOTE]
|
|
11
|
+
> Postmarker keeps Postmark's field names: `From`, `HtmlBody`, `TemplateModel`, `MessageStream`.
|
|
12
|
+
> Copy any field from the [Postmark API docs](https://postmarkapp.com/developer) and pass it as is. There are no snake_case aliases.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
uv pip install postmarker
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
To inline `<style>` CSS into HTML emails, install the `css-inline` extra and create the client with `inline_css=True`:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
uv pip install 'postmarker[css-inline]'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from postmarker.core import PostmarkClient
|
|
30
|
+
|
|
31
|
+
postmark = PostmarkClient(server_token="POSTMARK_API_TEST")
|
|
32
|
+
postmark.emails.send(
|
|
33
|
+
From="sender@example.com",
|
|
34
|
+
To="receiver@example.com",
|
|
35
|
+
Subject="Postmark test",
|
|
36
|
+
HtmlBody="<strong>Hello</strong> dear Postmark user.",
|
|
37
|
+
)
|
|
38
|
+
# {"ErrorCode": 0, "Message": "Test job accepted", "MessageID": "...", ...}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
With the `POSTMARK_API_TEST` token, Postmark validates the request without delivering it.
|
|
42
|
+
To send real emails, use the server token from your server's API Tokens tab in Postmark.
|
|
43
|
+
`From` must be a confirmed sender signature or an address on a verified domain.
|
|
44
|
+
|
|
45
|
+
Send with a template:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
postmark.emails.send_with_template(
|
|
49
|
+
TemplateAlias="welcome",
|
|
50
|
+
TemplateModel={"name": "Jane"},
|
|
51
|
+
From="sender@example.com",
|
|
52
|
+
To="receiver@example.com",
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Send a batch. Postmarker splits it into requests of 500 messages, Postmark's limit:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
messages = [
|
|
60
|
+
{"From": "sender@example.com", "To": to, "Subject": "Hi", "TextBody": "Hello"}
|
|
61
|
+
for to in ("first@example.com", "second@example.com")
|
|
62
|
+
]
|
|
63
|
+
responses = postmark.emails.send_batch(*messages)
|
|
64
|
+
# Postmark accepts or rejects each message separately
|
|
65
|
+
failed = [response for response in responses if response["ErrorCode"] != 0]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Add headers and attachments:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
email = postmark.emails.Email(
|
|
72
|
+
From="sender@example.com",
|
|
73
|
+
To="receiver@example.com",
|
|
74
|
+
Subject="Report",
|
|
75
|
+
TextBody="See the attached report.",
|
|
76
|
+
)
|
|
77
|
+
email["X-Accept-Language"] = "en-us, en"
|
|
78
|
+
email.attach("/home/user/report.pdf")
|
|
79
|
+
email.attach_binary(content=b"content", filename="readme.txt")
|
|
80
|
+
email.send()
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
A rejected request raises `ClientError` with Postmark's [error code](https://postmarkapp.com/developer/api/overview#error-codes):
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from postmarker.exceptions import ClientError
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
postmark.emails.send(
|
|
90
|
+
From="sender@example.com",
|
|
91
|
+
To="receiver@example.com",
|
|
92
|
+
TextBody="Hello",
|
|
93
|
+
)
|
|
94
|
+
except ClientError as exc:
|
|
95
|
+
print(exc.error_code) # For example, 406 for an inactive recipient
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Django
|
|
99
|
+
|
|
100
|
+
Django 6.1+:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
MAILERS = {
|
|
104
|
+
"default": {
|
|
105
|
+
"BACKEND": "postmarker.django.EmailBackend",
|
|
106
|
+
"OPTIONS": {"token": "<YOUR POSTMARK SERVER TOKEN>"},
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Django 5.2 and 6.0:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
EMAIL_BACKEND = "postmarker.django.EmailBackend"
|
|
115
|
+
POSTMARK = {"TOKEN": "<YOUR POSTMARK SERVER TOKEN>"}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Django then sends `send_mail`, `EmailMessage` and emails from third-party apps through Postmark.
|
|
119
|
+
The [Django guide](https://postmarker.readthedocs.io/en/stable/django/) covers tags, metadata, message streams and signals.
|
|
120
|
+
|
|
121
|
+
## Testing
|
|
122
|
+
|
|
123
|
+
Postmarker ships a `postmark_request` pytest fixture.
|
|
124
|
+
It mocks requests to Postmark and records what your code sent, through `PostmarkClient` or the Django backend:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
def test_welcome_email(postmark_request):
|
|
128
|
+
send_welcome_email("jane@example.com") # Your code that sends an email
|
|
129
|
+
assert postmark_request.call_args.kwargs["json"]["To"] == "jane@example.com"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Features
|
|
133
|
+
|
|
134
|
+
- Server APIs: Emails, Templates with layouts, Bounces, Messages (inbound, outbound, opens and clicks), Message Streams, Suppressions, Server, Stats and Triggers.
|
|
135
|
+
- Account APIs: Domains, Sender signatures, Servers and pushing templates between servers.
|
|
136
|
+
- Status and Spam check APIs.
|
|
137
|
+
- Webhook wrappers: Inbound, Delivery, Open, Click, Bounce, Spam complaint and Subscription change.
|
|
138
|
+
- Sending `email.message.Message` instances.
|
|
139
|
+
- CSS inlining via [css-inline](https://github.com/Stranger6667/css-inline).
|
|
140
|
+
- Django email backend and Tornado helper.
|
|
141
|
+
- Type annotations, with a `py.typed` marker.
|
|
142
|
+
|
|
143
|
+
## Documentation
|
|
144
|
+
|
|
145
|
+
<https://postmarker.readthedocs.io/en/stable/>
|
|
146
|
+
|
|
147
|
+
## Python support
|
|
148
|
+
|
|
149
|
+
Postmarker supports CPython 3.10 to 3.15 and PyPy 3.11. The Django email backend supports Django 5.2, 6.0 and 6.1.
|
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
css-inline = ["css-inline>=0.24"]
|
|
43
|
+
tests = ["postmarker[css-inline]", "coverage[toml]>=7.2", "pytest>=8", "pytest-django", "pytest-recording>=0.14", "tornado"]
|
|
44
|
+
docs = ["mkdocstrings[python]", "pymdown-extensions", "ruff", "zensical>=0.1.1,<0.2"]
|
|
45
|
+
|
|
46
|
+
[project.entry-points.pytest11]
|
|
47
|
+
postmark = "postmarker.pytest"
|
|
48
|
+
|
|
49
|
+
[project.urls]
|
|
50
|
+
Documentation = "https://postmarker.readthedocs.io/en/stable/"
|
|
51
|
+
Changelog = "https://github.com/Stranger6667/postmarker/blob/master/CHANGELOG.md"
|
|
52
|
+
"Bug Tracker" = "https://github.com/Stranger6667/postmarker/issues"
|
|
53
|
+
Funding = "https://github.com/sponsors/Stranger6667"
|
|
54
|
+
"Source Code" = "https://github.com/Stranger6667/postmarker"
|
|
55
|
+
|
|
56
|
+
[tool.hatch.version]
|
|
57
|
+
path = "src/postmarker/__init__.py"
|
|
58
|
+
|
|
59
|
+
[tool.hatch.build.targets.sdist]
|
|
60
|
+
include = ["/src", "/test"]
|
|
61
|
+
|
|
62
|
+
[tool.pytest.ini_options]
|
|
63
|
+
addopts = ["-ra", "--strict-markers", "--strict-config", "--block-network", "--allowed-hosts=127.0.0.1,localhost"]
|
|
64
|
+
xfail_strict = true
|
|
65
|
+
testpaths = ["test"]
|
|
66
|
+
pythonpath = ["."]
|
|
67
|
+
DJANGO_SETTINGS_MODULE = "test.django.settings"
|
|
68
|
+
|
|
69
|
+
[tool.coverage.run]
|
|
70
|
+
parallel = true
|
|
71
|
+
branch = true
|
|
72
|
+
source_pkgs = ["postmarker"]
|
|
73
|
+
|
|
74
|
+
[tool.coverage.paths]
|
|
75
|
+
source = ["src", ".tox/*/site-packages", ".tox/*/lib/*/site-packages", ".tox/*/Lib/site-packages"]
|
|
76
|
+
|
|
77
|
+
[tool.coverage.report]
|
|
78
|
+
show_missing = true
|
|
79
|
+
precision = 2
|
|
80
|
+
exclude_also = ["if TYPE_CHECKING:"]
|
|
81
|
+
fail_under = 100
|
|
82
|
+
|
|
83
|
+
[tool.ruff]
|
|
84
|
+
line-length = 120
|
|
85
|
+
target-version = "py310"
|
|
86
|
+
|
|
87
|
+
[tool.ruff.lint]
|
|
88
|
+
select = [
|
|
89
|
+
"E", # pycodestyle errors
|
|
90
|
+
"W", # pycodestyle warnings
|
|
91
|
+
"F", # pyflakes
|
|
92
|
+
"C", # flake8-comprehensions
|
|
93
|
+
"B", # flake8-bugbear
|
|
94
|
+
"D", # pydocstyle
|
|
95
|
+
"T201", # flake8-print
|
|
96
|
+
"I", # isort
|
|
97
|
+
"FURB", # refurb
|
|
98
|
+
"PGH003", # blanket-type-ignore
|
|
99
|
+
"UP", # pyupgrade
|
|
100
|
+
"SIM", # flake8-simplify
|
|
101
|
+
]
|
|
102
|
+
ignore = [
|
|
103
|
+
"E501", # Line too long
|
|
104
|
+
"B008", # Do not perform function calls in argument defaults
|
|
105
|
+
"C901", # Too complex
|
|
106
|
+
"D100", # Missing docstring in public module
|
|
107
|
+
"D101", # Missing docstring in public class
|
|
108
|
+
"D102", # Missing docstring in public method
|
|
109
|
+
"D103", # Missing docstring in public function
|
|
110
|
+
"D104", # Missing docstring in public package
|
|
111
|
+
"D105", # Missing docstring in magic method
|
|
112
|
+
"D106", # Missing docstring in public nested class
|
|
113
|
+
"D107", # Missing docstring in `__init__`
|
|
114
|
+
"D202", # No blank lines allowed after function docstring
|
|
115
|
+
"D203", # One blank line before class
|
|
116
|
+
"D213", # Multiline summary second line
|
|
117
|
+
"D401", # Imperative mood
|
|
118
|
+
"D407", # Missing dashed underline after section
|
|
119
|
+
"SIM105", # contextlib.suppress is less performant in many cases
|
|
120
|
+
"SIM108", # ternary expressions are not always readable
|
|
121
|
+
"SIM910", # delete default arguments in .get() in some cases
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
[tool.ruff.lint.isort]
|
|
125
|
+
known-first-party = ["postmarker"]
|
|
126
|
+
|
|
127
|
+
[tool.typos.default]
|
|
128
|
+
# Verbatim Postmark spam check API message
|
|
129
|
+
extend-ignore-re = ["SpamAssassin error occured"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.1"
|