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.
Files changed (159) hide show
  1. postmarker-2.0/.gitignore +94 -0
  2. postmarker-2.0/PKG-INFO +146 -0
  3. postmarker-2.0/README.md +95 -0
  4. postmarker-2.0/pyproject.toml +127 -0
  5. postmarker-2.0/src/postmarker/__init__.py +1 -0
  6. {postmarker-1.0 → postmarker-2.0}/src/postmarker/core.py +60 -24
  7. postmarker-2.0/src/postmarker/django/__init__.py +12 -0
  8. {postmarker-1.0 → postmarker-2.0}/src/postmarker/django/backend.py +27 -16
  9. {postmarker-1.0 → postmarker-2.0}/src/postmarker/exceptions.py +6 -1
  10. {postmarker-1.0 → postmarker-2.0}/src/postmarker/logging.py +4 -1
  11. postmarker-2.0/src/postmarker/models/base.py +134 -0
  12. {postmarker-1.0 → postmarker-2.0}/src/postmarker/models/bounces.py +43 -22
  13. postmarker-2.0/src/postmarker/models/domains.py +75 -0
  14. {postmarker-1.0 → postmarker-2.0}/src/postmarker/models/emails.py +185 -141
  15. postmarker-2.0/src/postmarker/models/message_streams.py +95 -0
  16. {postmarker-1.0 → postmarker-2.0}/src/postmarker/models/messages.py +153 -76
  17. postmarker-2.0/src/postmarker/models/senders.py +106 -0
  18. {postmarker-1.0 → postmarker-2.0}/src/postmarker/models/server.py +9 -5
  19. postmarker-2.0/src/postmarker/models/servers.py +55 -0
  20. {postmarker-1.0 → postmarker-2.0}/src/postmarker/models/stats.py +19 -15
  21. {postmarker-1.0 → postmarker-2.0}/src/postmarker/models/status.py +22 -15
  22. postmarker-2.0/src/postmarker/models/suppressions.py +95 -0
  23. postmarker-2.0/src/postmarker/models/templates.py +155 -0
  24. {postmarker-1.0 → postmarker-2.0}/src/postmarker/models/triggers.py +27 -22
  25. postmarker-2.0/src/postmarker/py.typed +0 -0
  26. postmarker-2.0/src/postmarker/pytest.py +34 -0
  27. {postmarker-1.0 → postmarker-2.0}/src/postmarker/tornado.py +15 -4
  28. {postmarker-1.0 → postmarker-2.0}/src/postmarker/utils.py +9 -3
  29. postmarker-2.0/test/__init__.py +0 -0
  30. postmarker-2.0/test/cassettes/test_exceptions/test_raise.yaml +37 -0
  31. postmarker-2.0/test/cassettes/test_spamcheck/test_spamcheck_invalid.yaml +41 -0
  32. postmarker-2.0/test/cassettes/test_spamcheck/test_spamcheck_valid.yaml +60 -0
  33. postmarker-2.0/test/conftest.py +239 -0
  34. postmarker-2.0/test/django/__init__.py +0 -0
  35. postmarker-2.0/test/django/conftest.py +11 -0
  36. postmarker-2.0/test/django/settings.py +4 -0
  37. postmarker-2.0/test/django/test_backend.py +552 -0
  38. postmarker-2.0/test/django/test_mixins.py +92 -0
  39. postmarker-2.0/test/models/__init__.py +0 -0
  40. postmarker-2.0/test/models/attachments/readme.txt +1 -0
  41. postmarker-2.0/test/models/attachments/report.blabla +1 -0
  42. postmarker-2.0/test/models/cassettes/test_bounces/test_load_all_bounces_multiple_calls[2-1-2].yaml +77 -0
  43. postmarker-2.0/test/models/cassettes/test_bounces/test_load_all_bounces_multiple_calls[2-50-1].yaml +42 -0
  44. postmarker-2.0/test/models/cassettes/test_bounces/test_load_all_bounces_multiple_calls[4-1-2].yaml +77 -0
  45. postmarker-2.0/test/models/cassettes/test_bounces/test_load_all_bounces_multiple_calls[None-1-2].yaml +77 -0
  46. postmarker-2.0/test/models/cassettes/test_bounces/test_manager_all.yaml +42 -0
  47. postmarker-2.0/test/models/cassettes/test_bounces/test_manager_deliverystats.yaml +38 -0
  48. postmarker-2.0/test/models/cassettes/test_bounces/test_manager_tags.yaml +36 -0
  49. postmarker-2.0/test/models/cassettes/test_bounces/test_message.yaml +78 -0
  50. postmarker-2.0/test/models/cassettes/test_bounces/test_model_activate.yaml +82 -0
  51. postmarker-2.0/test/models/cassettes/test_bounces/test_model_dump.yaml +75 -0
  52. postmarker-2.0/test/models/cassettes/test_bounces/test_model_get.yaml +41 -0
  53. postmarker-2.0/test/models/cassettes/test_bounces/test_model_repr.yaml +41 -0
  54. postmarker-2.0/test/models/cassettes/test_bounces/test_model_str.yaml +41 -0
  55. postmarker-2.0/test/models/cassettes/test_domains/test_manager_all.yaml +36 -0
  56. postmarker-2.0/test/models/cassettes/test_domains/test_manager_domain_methods.yaml +279 -0
  57. postmarker-2.0/test/models/cassettes/test_domains/test_model_get.yaml +74 -0
  58. postmarker-2.0/test/models/cassettes/test_domains/test_model_str.yaml +38 -0
  59. postmarker-2.0/test/models/cassettes/test_emails/test_simple_send_default.yaml +44 -0
  60. postmarker-2.0/test/models/cassettes/test_emails/test_simple_send_mime_text.yaml +43 -0
  61. postmarker-2.0/test/models/cassettes/test_emails/test_simple_send_minimum_mime.yaml +43 -0
  62. postmarker-2.0/test/models/cassettes/test_emails/test_simple_send_with_template.yaml +40 -0
  63. postmarker-2.0/test/models/cassettes/test_messages/test_inbound_message_bypass.yaml +39 -0
  64. postmarker-2.0/test/models/cassettes/test_messages/test_inbound_message_retry.yaml +39 -0
  65. postmarker-2.0/test/models/cassettes/test_messages/test_inbound_messages_all.yaml +36 -0
  66. postmarker-2.0/test/models/cassettes/test_messages/test_opens_model.yaml +37 -0
  67. postmarker-2.0/test/models/cassettes/test_messages/test_outbound_message.yaml +37 -0
  68. postmarker-2.0/test/models/cassettes/test_messages/test_outbound_message_get.yaml +75 -0
  69. postmarker-2.0/test/models/cassettes/test_messages/test_outbound_message_get_dump.yaml +71 -0
  70. postmarker-2.0/test/models/cassettes/test_messages/test_outbound_message_opens.yaml +71 -0
  71. postmarker-2.0/test/models/cassettes/test_senders/test_all.yaml +38 -0
  72. postmarker-2.0/test/models/cassettes/test_senders/test_all_methods.yaml +190 -0
  73. postmarker-2.0/test/models/cassettes/test_senders/test_edit.yaml +92 -0
  74. postmarker-2.0/test/models/cassettes/test_senders/test_get.yaml +88 -0
  75. postmarker-2.0/test/models/cassettes/test_server/test_edit.yaml +74 -0
  76. postmarker-2.0/test/models/cassettes/test_server/test_str.yaml +36 -0
  77. postmarker-2.0/test/models/cassettes/test_stats/test_methods[bounces-kwargs2-expected2].yaml +36 -0
  78. postmarker-2.0/test/models/cassettes/test_stats/test_methods[browserfamilies-kwargs10-expected10].yaml +36 -0
  79. postmarker-2.0/test/models/cassettes/test_stats/test_methods[clicks-kwargs9-expected9].yaml +36 -0
  80. postmarker-2.0/test/models/cassettes/test_stats/test_methods[clicks_platforms-kwargs11-expected11].yaml +36 -0
  81. postmarker-2.0/test/models/cassettes/test_stats/test_methods[emailclients-kwargs7-expected7].yaml +36 -0
  82. postmarker-2.0/test/models/cassettes/test_stats/test_methods[location-kwargs12-expected12].yaml +36 -0
  83. postmarker-2.0/test/models/cassettes/test_stats/test_methods[opens-kwargs5-expected5].yaml +36 -0
  84. postmarker-2.0/test/models/cassettes/test_stats/test_methods[opens_platforms-kwargs6-expected6].yaml +36 -0
  85. postmarker-2.0/test/models/cassettes/test_stats/test_methods[overview-kwargs0-expected0].yaml +36 -0
  86. postmarker-2.0/test/models/cassettes/test_stats/test_methods[readtimes-kwargs8-expected8].yaml +36 -0
  87. postmarker-2.0/test/models/cassettes/test_stats/test_methods[sends-kwargs1-expected1].yaml +36 -0
  88. postmarker-2.0/test/models/cassettes/test_stats/test_methods[spam-kwargs3-expected3].yaml +36 -0
  89. postmarker-2.0/test/models/cassettes/test_stats/test_methods[tracked-kwargs4-expected4].yaml +36 -0
  90. postmarker-2.0/test/models/cassettes/test_status/test_availability.yaml +40 -0
  91. postmarker-2.0/test/models/cassettes/test_status/test_delivery.yaml +40 -0
  92. postmarker-2.0/test/models/cassettes/test_status/test_get_incident.yaml +46 -0
  93. postmarker-2.0/test/models/cassettes/test_status/test_incidents.yaml +346 -0
  94. postmarker-2.0/test/models/cassettes/test_status/test_last_incident.yaml +46 -0
  95. postmarker-2.0/test/models/cassettes/test_status/test_services.yaml +43 -0
  96. postmarker-2.0/test/models/cassettes/test_status/test_status.yaml +40 -0
  97. postmarker-2.0/test/models/cassettes/test_templates/test_manager_all.yaml +36 -0
  98. postmarker-2.0/test/models/cassettes/test_templates/test_manager_create.yaml +77 -0
  99. postmarker-2.0/test/models/cassettes/test_templates/test_manager_edit.yaml +40 -0
  100. postmarker-2.0/test/models/cassettes/test_templates/test_manager_validate.yaml +41 -0
  101. postmarker-2.0/test/models/cassettes/test_templates/test_model_default.yaml +40 -0
  102. postmarker-2.0/test/models/cassettes/test_templates/test_model_edit.yaml +78 -0
  103. postmarker-2.0/test/models/cassettes/test_templates/test_model_get.yaml +78 -0
  104. postmarker-2.0/test/models/cassettes/test_templates/test_model_repr.yaml +40 -0
  105. postmarker-2.0/test/models/cassettes/test_triggers/test_inbound_rules_all.yaml +36 -0
  106. postmarker-2.0/test/models/cassettes/test_triggers/test_inbound_rules_create.yaml +40 -0
  107. postmarker-2.0/test/models/cassettes/test_triggers/test_inbound_rules_delete.yaml +72 -0
  108. postmarker-2.0/test/models/cassettes/test_triggers/test_tags_all.yaml +36 -0
  109. postmarker-2.0/test/models/cassettes/test_triggers/test_tags_create.yaml +40 -0
  110. postmarker-2.0/test/models/cassettes/test_triggers/test_tags_delete.yaml +72 -0
  111. postmarker-2.0/test/models/cassettes/test_triggers/test_tags_edit.yaml +78 -0
  112. postmarker-2.0/test/models/cassettes/test_triggers/test_tags_get.yaml +70 -0
  113. postmarker-2.0/test/models/test_base.py +72 -0
  114. postmarker-2.0/test/models/test_bounces.py +80 -0
  115. postmarker-2.0/test/models/test_clicks.py +244 -0
  116. postmarker-2.0/test/models/test_domains.py +76 -0
  117. postmarker-2.0/test/models/test_emails.py +487 -0
  118. postmarker-2.0/test/models/test_message_streams.py +251 -0
  119. postmarker-2.0/test/models/test_messages.py +180 -0
  120. postmarker-2.0/test/models/test_senders.py +105 -0
  121. postmarker-2.0/test/models/test_server.py +13 -0
  122. postmarker-2.0/test/models/test_servers.py +158 -0
  123. postmarker-2.0/test/models/test_stats.py +130 -0
  124. postmarker-2.0/test/models/test_status.py +92 -0
  125. postmarker-2.0/test/models/test_suppressions.py +180 -0
  126. postmarker-2.0/test/models/test_templates.py +232 -0
  127. postmarker-2.0/test/models/test_triggers.py +58 -0
  128. postmarker-2.0/test/models/test_webhooks.py +103 -0
  129. postmarker-2.0/test/test_core.py +106 -0
  130. {postmarker-1.0 → postmarker-2.0}/test/test_exceptions.py +1 -1
  131. postmarker-2.0/test/test_pytest.py +18 -0
  132. {postmarker-1.0 → postmarker-2.0}/test/test_spamcheck.py +1 -1
  133. postmarker-2.0/test/tornado/__init__.py +0 -0
  134. postmarker-2.0/test/tornado/test_handlers.py +102 -0
  135. postmarker-2.0/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.0}/LICENSE +0 -0
  157. {postmarker-1.0 → postmarker-2.0}/src/postmarker/django/signals.py +0 -0
  158. {postmarker-1.0 → postmarker-2.0}/src/postmarker/models/__init__.py +0 -0
  159. {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/
@@ -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
+ [![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)
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 :)
@@ -0,0 +1,95 @@
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 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 = "http://spamcheck.postmarkapp.com/filter/"
24
- USER_AGENT = "Postmarker/%s" % __version__
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
- _managers = (
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 token to use Postmark API"
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(cls, config, prefix="postmark_", is_uppercase=False):
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() # pylint: disable=attribute-defined-outside-init
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(self, method, root, endpoint, data=None, headers=None, **kwargs):
120
- default_headers = {"Accept": "application/json", "User-Agent": USER_AGENT}
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
+ ]