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.
Files changed (159) hide show
  1. postmarker-2.1/.gitignore +94 -0
  2. postmarker-2.1/PKG-INFO +203 -0
  3. postmarker-2.1/README.md +149 -0
  4. postmarker-2.1/pyproject.toml +129 -0
  5. postmarker-2.1/src/postmarker/__init__.py +1 -0
  6. {postmarker-1.0 → postmarker-2.1}/src/postmarker/core.py +80 -24
  7. postmarker-2.1/src/postmarker/django/__init__.py +12 -0
  8. {postmarker-1.0 → postmarker-2.1}/src/postmarker/django/backend.py +36 -19
  9. {postmarker-1.0 → postmarker-2.1}/src/postmarker/exceptions.py +6 -1
  10. {postmarker-1.0 → postmarker-2.1}/src/postmarker/logging.py +4 -1
  11. postmarker-2.1/src/postmarker/models/base.py +134 -0
  12. {postmarker-1.0 → postmarker-2.1}/src/postmarker/models/bounces.py +43 -22
  13. postmarker-2.1/src/postmarker/models/domains.py +75 -0
  14. {postmarker-1.0 → postmarker-2.1}/src/postmarker/models/emails.py +193 -142
  15. postmarker-2.1/src/postmarker/models/message_streams.py +95 -0
  16. {postmarker-1.0 → postmarker-2.1}/src/postmarker/models/messages.py +153 -76
  17. postmarker-2.1/src/postmarker/models/senders.py +106 -0
  18. {postmarker-1.0 → postmarker-2.1}/src/postmarker/models/server.py +9 -5
  19. postmarker-2.1/src/postmarker/models/servers.py +55 -0
  20. {postmarker-1.0 → postmarker-2.1}/src/postmarker/models/stats.py +19 -15
  21. {postmarker-1.0 → postmarker-2.1}/src/postmarker/models/status.py +22 -15
  22. postmarker-2.1/src/postmarker/models/suppressions.py +95 -0
  23. postmarker-2.1/src/postmarker/models/templates.py +155 -0
  24. {postmarker-1.0 → postmarker-2.1}/src/postmarker/models/triggers.py +27 -22
  25. postmarker-2.1/src/postmarker/py.typed +0 -0
  26. postmarker-2.1/src/postmarker/pytest.py +34 -0
  27. {postmarker-1.0 → postmarker-2.1}/src/postmarker/tornado.py +15 -4
  28. {postmarker-1.0 → postmarker-2.1}/src/postmarker/utils.py +9 -3
  29. postmarker-2.1/test/__init__.py +0 -0
  30. postmarker-2.1/test/cassettes/test_exceptions/test_raise.yaml +37 -0
  31. postmarker-2.1/test/cassettes/test_spamcheck/test_spamcheck_invalid.yaml +41 -0
  32. postmarker-2.1/test/cassettes/test_spamcheck/test_spamcheck_valid.yaml +60 -0
  33. postmarker-2.1/test/conftest.py +239 -0
  34. postmarker-2.1/test/django/__init__.py +0 -0
  35. postmarker-2.1/test/django/conftest.py +11 -0
  36. postmarker-2.1/test/django/settings.py +4 -0
  37. postmarker-2.1/test/django/test_backend.py +573 -0
  38. postmarker-2.1/test/django/test_mixins.py +92 -0
  39. postmarker-2.1/test/models/__init__.py +0 -0
  40. postmarker-2.1/test/models/attachments/readme.txt +1 -0
  41. postmarker-2.1/test/models/attachments/report.blabla +1 -0
  42. postmarker-2.1/test/models/cassettes/test_bounces/test_load_all_bounces_multiple_calls[2-1-2].yaml +77 -0
  43. postmarker-2.1/test/models/cassettes/test_bounces/test_load_all_bounces_multiple_calls[2-50-1].yaml +42 -0
  44. postmarker-2.1/test/models/cassettes/test_bounces/test_load_all_bounces_multiple_calls[4-1-2].yaml +77 -0
  45. postmarker-2.1/test/models/cassettes/test_bounces/test_load_all_bounces_multiple_calls[None-1-2].yaml +77 -0
  46. postmarker-2.1/test/models/cassettes/test_bounces/test_manager_all.yaml +42 -0
  47. postmarker-2.1/test/models/cassettes/test_bounces/test_manager_deliverystats.yaml +38 -0
  48. postmarker-2.1/test/models/cassettes/test_bounces/test_manager_tags.yaml +36 -0
  49. postmarker-2.1/test/models/cassettes/test_bounces/test_message.yaml +78 -0
  50. postmarker-2.1/test/models/cassettes/test_bounces/test_model_activate.yaml +82 -0
  51. postmarker-2.1/test/models/cassettes/test_bounces/test_model_dump.yaml +75 -0
  52. postmarker-2.1/test/models/cassettes/test_bounces/test_model_get.yaml +41 -0
  53. postmarker-2.1/test/models/cassettes/test_bounces/test_model_repr.yaml +41 -0
  54. postmarker-2.1/test/models/cassettes/test_bounces/test_model_str.yaml +41 -0
  55. postmarker-2.1/test/models/cassettes/test_domains/test_manager_all.yaml +36 -0
  56. postmarker-2.1/test/models/cassettes/test_domains/test_manager_domain_methods.yaml +279 -0
  57. postmarker-2.1/test/models/cassettes/test_domains/test_model_get.yaml +74 -0
  58. postmarker-2.1/test/models/cassettes/test_domains/test_model_str.yaml +38 -0
  59. postmarker-2.1/test/models/cassettes/test_emails/test_simple_send_default.yaml +44 -0
  60. postmarker-2.1/test/models/cassettes/test_emails/test_simple_send_mime_text.yaml +43 -0
  61. postmarker-2.1/test/models/cassettes/test_emails/test_simple_send_minimum_mime.yaml +43 -0
  62. postmarker-2.1/test/models/cassettes/test_emails/test_simple_send_with_template.yaml +40 -0
  63. postmarker-2.1/test/models/cassettes/test_messages/test_inbound_message_bypass.yaml +39 -0
  64. postmarker-2.1/test/models/cassettes/test_messages/test_inbound_message_retry.yaml +39 -0
  65. postmarker-2.1/test/models/cassettes/test_messages/test_inbound_messages_all.yaml +36 -0
  66. postmarker-2.1/test/models/cassettes/test_messages/test_opens_model.yaml +37 -0
  67. postmarker-2.1/test/models/cassettes/test_messages/test_outbound_message.yaml +37 -0
  68. postmarker-2.1/test/models/cassettes/test_messages/test_outbound_message_get.yaml +75 -0
  69. postmarker-2.1/test/models/cassettes/test_messages/test_outbound_message_get_dump.yaml +71 -0
  70. postmarker-2.1/test/models/cassettes/test_messages/test_outbound_message_opens.yaml +71 -0
  71. postmarker-2.1/test/models/cassettes/test_senders/test_all.yaml +38 -0
  72. postmarker-2.1/test/models/cassettes/test_senders/test_all_methods.yaml +190 -0
  73. postmarker-2.1/test/models/cassettes/test_senders/test_edit.yaml +92 -0
  74. postmarker-2.1/test/models/cassettes/test_senders/test_get.yaml +88 -0
  75. postmarker-2.1/test/models/cassettes/test_server/test_edit.yaml +74 -0
  76. postmarker-2.1/test/models/cassettes/test_server/test_str.yaml +36 -0
  77. postmarker-2.1/test/models/cassettes/test_stats/test_methods[bounces-kwargs2-expected2].yaml +36 -0
  78. postmarker-2.1/test/models/cassettes/test_stats/test_methods[browserfamilies-kwargs10-expected10].yaml +36 -0
  79. postmarker-2.1/test/models/cassettes/test_stats/test_methods[clicks-kwargs9-expected9].yaml +36 -0
  80. postmarker-2.1/test/models/cassettes/test_stats/test_methods[clicks_platforms-kwargs11-expected11].yaml +36 -0
  81. postmarker-2.1/test/models/cassettes/test_stats/test_methods[emailclients-kwargs7-expected7].yaml +36 -0
  82. postmarker-2.1/test/models/cassettes/test_stats/test_methods[location-kwargs12-expected12].yaml +36 -0
  83. postmarker-2.1/test/models/cassettes/test_stats/test_methods[opens-kwargs5-expected5].yaml +36 -0
  84. postmarker-2.1/test/models/cassettes/test_stats/test_methods[opens_platforms-kwargs6-expected6].yaml +36 -0
  85. postmarker-2.1/test/models/cassettes/test_stats/test_methods[overview-kwargs0-expected0].yaml +36 -0
  86. postmarker-2.1/test/models/cassettes/test_stats/test_methods[readtimes-kwargs8-expected8].yaml +36 -0
  87. postmarker-2.1/test/models/cassettes/test_stats/test_methods[sends-kwargs1-expected1].yaml +36 -0
  88. postmarker-2.1/test/models/cassettes/test_stats/test_methods[spam-kwargs3-expected3].yaml +36 -0
  89. postmarker-2.1/test/models/cassettes/test_stats/test_methods[tracked-kwargs4-expected4].yaml +36 -0
  90. postmarker-2.1/test/models/cassettes/test_status/test_availability.yaml +40 -0
  91. postmarker-2.1/test/models/cassettes/test_status/test_delivery.yaml +40 -0
  92. postmarker-2.1/test/models/cassettes/test_status/test_get_incident.yaml +46 -0
  93. postmarker-2.1/test/models/cassettes/test_status/test_incidents.yaml +346 -0
  94. postmarker-2.1/test/models/cassettes/test_status/test_last_incident.yaml +46 -0
  95. postmarker-2.1/test/models/cassettes/test_status/test_services.yaml +43 -0
  96. postmarker-2.1/test/models/cassettes/test_status/test_status.yaml +40 -0
  97. postmarker-2.1/test/models/cassettes/test_templates/test_manager_all.yaml +36 -0
  98. postmarker-2.1/test/models/cassettes/test_templates/test_manager_create.yaml +77 -0
  99. postmarker-2.1/test/models/cassettes/test_templates/test_manager_edit.yaml +40 -0
  100. postmarker-2.1/test/models/cassettes/test_templates/test_manager_validate.yaml +41 -0
  101. postmarker-2.1/test/models/cassettes/test_templates/test_model_default.yaml +40 -0
  102. postmarker-2.1/test/models/cassettes/test_templates/test_model_edit.yaml +78 -0
  103. postmarker-2.1/test/models/cassettes/test_templates/test_model_get.yaml +78 -0
  104. postmarker-2.1/test/models/cassettes/test_templates/test_model_repr.yaml +40 -0
  105. postmarker-2.1/test/models/cassettes/test_triggers/test_inbound_rules_all.yaml +36 -0
  106. postmarker-2.1/test/models/cassettes/test_triggers/test_inbound_rules_create.yaml +40 -0
  107. postmarker-2.1/test/models/cassettes/test_triggers/test_inbound_rules_delete.yaml +72 -0
  108. postmarker-2.1/test/models/cassettes/test_triggers/test_tags_all.yaml +36 -0
  109. postmarker-2.1/test/models/cassettes/test_triggers/test_tags_create.yaml +40 -0
  110. postmarker-2.1/test/models/cassettes/test_triggers/test_tags_delete.yaml +72 -0
  111. postmarker-2.1/test/models/cassettes/test_triggers/test_tags_edit.yaml +78 -0
  112. postmarker-2.1/test/models/cassettes/test_triggers/test_tags_get.yaml +70 -0
  113. postmarker-2.1/test/models/test_base.py +72 -0
  114. postmarker-2.1/test/models/test_bounces.py +80 -0
  115. postmarker-2.1/test/models/test_clicks.py +244 -0
  116. postmarker-2.1/test/models/test_domains.py +76 -0
  117. postmarker-2.1/test/models/test_emails.py +579 -0
  118. postmarker-2.1/test/models/test_message_streams.py +251 -0
  119. postmarker-2.1/test/models/test_messages.py +180 -0
  120. postmarker-2.1/test/models/test_senders.py +105 -0
  121. postmarker-2.1/test/models/test_server.py +13 -0
  122. postmarker-2.1/test/models/test_servers.py +158 -0
  123. postmarker-2.1/test/models/test_stats.py +130 -0
  124. postmarker-2.1/test/models/test_status.py +92 -0
  125. postmarker-2.1/test/models/test_suppressions.py +180 -0
  126. postmarker-2.1/test/models/test_templates.py +232 -0
  127. postmarker-2.1/test/models/test_triggers.py +58 -0
  128. postmarker-2.1/test/models/test_webhooks.py +103 -0
  129. postmarker-2.1/test/test_core.py +106 -0
  130. {postmarker-1.0 → postmarker-2.1}/test/test_exceptions.py +1 -1
  131. postmarker-2.1/test/test_pytest.py +18 -0
  132. {postmarker-1.0 → postmarker-2.1}/test/test_spamcheck.py +1 -1
  133. postmarker-2.1/test/tornado/__init__.py +0 -0
  134. postmarker-2.1/test/tornado/test_handlers.py +102 -0
  135. postmarker-2.1/test/typing_checks.py +24 -0
  136. postmarker-1.0/PKG-INFO +0 -147
  137. postmarker-1.0/README.rst +0 -115
  138. postmarker-1.0/pyproject.toml +0 -16
  139. postmarker-1.0/setup.cfg +0 -4
  140. postmarker-1.0/setup.py +0 -45
  141. postmarker-1.0/src/postmarker/__init__.py +0 -1
  142. postmarker-1.0/src/postmarker/django/__init__.py +0 -2
  143. postmarker-1.0/src/postmarker/models/base.py +0 -118
  144. postmarker-1.0/src/postmarker/models/domains.py +0 -71
  145. postmarker-1.0/src/postmarker/models/senders.py +0 -82
  146. postmarker-1.0/src/postmarker/models/templates.py +0 -85
  147. postmarker-1.0/src/postmarker/pytest.py +0 -20
  148. postmarker-1.0/src/postmarker.egg-info/PKG-INFO +0 -147
  149. postmarker-1.0/src/postmarker.egg-info/SOURCES.txt +0 -37
  150. postmarker-1.0/src/postmarker.egg-info/dependency_links.txt +0 -1
  151. postmarker-1.0/src/postmarker.egg-info/entry_points.txt +0 -3
  152. postmarker-1.0/src/postmarker.egg-info/requires.txt +0 -1
  153. postmarker-1.0/src/postmarker.egg-info/top_level.txt +0 -1
  154. postmarker-1.0/test/test_core.py +0 -70
  155. postmarker-1.0/test/test_pytest.py +0 -6
  156. {postmarker-1.0 → postmarker-2.1}/LICENSE +0 -0
  157. {postmarker-1.0 → postmarker-2.1}/src/postmarker/django/signals.py +0 -0
  158. {postmarker-1.0 → postmarker-2.1}/src/postmarker/models/__init__.py +0 -0
  159. {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/
@@ -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
+ [![Build](https://github.com/Stranger6667/postmarker/actions/workflows/build.yml/badge.svg)](https://github.com/Stranger6667/postmarker/actions/workflows/build.yml) [![Coverage](https://codecov.io/gh/Stranger6667/postmarker/branch/master/graph/badge.svg)](https://codecov.io/gh/Stranger6667/postmarker) [![Version](https://img.shields.io/pypi/v/postmarker.svg)](https://pypi.org/project/postmarker/) [![Python versions](https://img.shields.io/pypi/pyversions/postmarker.svg)](https://pypi.org/project/postmarker/) [![Docs](https://readthedocs.org/projects/postmarker/badge/?version=stable)](https://postmarker.readthedocs.io/en/stable/) [![License](https://img.shields.io/pypi/l/postmarker.svg)](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.
@@ -0,0 +1,149 @@
1
+ # Postmarker
2
+
3
+ [![Build](https://github.com/Stranger6667/postmarker/actions/workflows/build.yml/badge.svg)](https://github.com/Stranger6667/postmarker/actions/workflows/build.yml) [![Coverage](https://codecov.io/gh/Stranger6667/postmarker/branch/master/graph/badge.svg)](https://codecov.io/gh/Stranger6667/postmarker) [![Version](https://img.shields.io/pypi/v/postmarker.svg)](https://pypi.org/project/postmarker/) [![Python versions](https://img.shields.io/pypi/pyversions/postmarker.svg)](https://pypi.org/project/postmarker/) [![Docs](https://readthedocs.org/projects/postmarker/badge/?version=stable)](https://postmarker.readthedocs.io/en/stable/) [![License](https://img.shields.io/pypi/l/postmarker.svg)](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"