mailhive 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ .venv/
2
+ dist/
3
+ __pycache__/
4
+ *.egg-info/
5
+ .pytest_cache/
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (unreleased)
4
+
5
+ - `Mailhive` and `AsyncMailhive`, with `emails.send`, `emails.send_batch` and `emails.get`.
6
+ - Idempotency keys reused across retries, and `Retry-After` honoured.
7
+ - Typed errors that carry `request_id`.
8
+ - `verify_webhook`, with secret rotation.
9
+ - The Django email backend `mailhive.django.EmailBackend`.
mailhive-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Naszat
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.5
2
+ Name: mailhive
3
+ Version: 0.1.0
4
+ Summary: The official Python SDK for Mailhive Send: transactional email from Africa.
5
+ Project-URL: Homepage, https://mailhive.africa/docs/sdks/python
6
+ Project-URL: Source, https://github.com/Naszat/mailhive-sdks/tree/main/python
7
+ Project-URL: Issues, https://github.com/Naszat/mailhive-sdks/issues
8
+ Author: Naszat
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: africa,django,email,mailhive,transactional-email
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Framework :: Django
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Communications :: Email
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.9
19
+ Requires-Dist: httpx<1,>=0.25
20
+ Requires-Dist: typing-extensions>=4.7; python_version < '3.11'
21
+ Provides-Extra: django
22
+ Requires-Dist: django>=4.2; extra == 'django'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # mailhive
26
+
27
+ The official Python SDK for [Mailhive Send](https://mailhive.africa/docs/send/overview). It supports Python 3.9+, with sync and async clients and a Django email backend.
28
+
29
+ ```sh
30
+ pip install mailhive
31
+ ```
32
+
33
+ ## Send an email
34
+
35
+ ```python
36
+ from mailhive import Mailhive
37
+
38
+ client = Mailhive() # reads MAILHIVE_API_KEY
39
+
40
+ email = client.emails.send({
41
+ "from": "Acme <hello@acme.com>",
42
+ "to": "ada@example.com",
43
+ "subject": "Your receipt",
44
+ "html": "<p>Thanks for your order.</p>",
45
+ })
46
+ print(email["id"])
47
+ ```
48
+
49
+ The keys match the [API reference](https://mailhive.africa/docs/api/send-email) exactly: `cc`, `bcc`, `reply_to`, `headers`, `tags`, `template_id`, `variables` and `attachments`. You can also pass keyword arguments, with `from_` standing in for `from`:
50
+
51
+ ```python
52
+ client.emails.send(from_="hello@acme.com", to="ada@example.com", subject="Hi", text="Hello")
53
+ ```
54
+
55
+ Attachment `content` can be `bytes`, which are encoded for you, or base64 text.
56
+
57
+ ```python
58
+ client.emails.send_batch([email1, email2]) # up to 100; all accepted or none
59
+ client.emails.get(email_id) # status: "delivered", "bounced", …
60
+ ```
61
+
62
+ ### Async
63
+
64
+ ```python
65
+ from mailhive import AsyncMailhive
66
+
67
+ async with AsyncMailhive() as client:
68
+ await client.emails.send({...})
69
+ ```
70
+
71
+ ## Retries and idempotency
72
+
73
+ Every send carries an `Idempotency-Key`. Retries reuse it, so **a retry never sends twice**.
74
+
75
+ - **Retried** (up to 2 times, then configurable): network errors, timeouts, 5xx responses, and `429 rate_limited` after the `Retry-After` delay.
76
+ - **Not retried:** a used-up allowance (`monthly_quota_reached`, `daily_cap_reached`) and invalid requests.
77
+
78
+ To stay safe across restarts, pass your own key:
79
+
80
+ ```python
81
+ client.emails.send(email, idempotency_key=f"order-{order.id}-receipt")
82
+ ```
83
+
84
+ ## Errors
85
+
86
+ ```python
87
+ from mailhive import RateLimitError, ValidationError
88
+
89
+ try:
90
+ client.emails.send(email)
91
+ except ValidationError as error:
92
+ print(error.code, error.message, error.details, error.request_id)
93
+ except RateLimitError as error:
94
+ if error.code == "monthly_quota_reached":
95
+ ...
96
+ ```
97
+
98
+ | Exception | Status |
99
+ |---|---|
100
+ | `AuthenticationError` | 401 |
101
+ | `BillingError` | 402 |
102
+ | `PermissionDeniedError` | 403 |
103
+ | `NotFoundError` | 404 |
104
+ | `ConflictError` | 409 (e.g. `idempotency_conflict`) |
105
+ | `ValidationError` | 422 |
106
+ | `RateLimitError` | 429 (`retry_after` in seconds) |
107
+ | `APIError` | other statuses, and the base class of all of the above |
108
+ | `APIConnectionError` | no response |
109
+
110
+ All of them subclass `MailhiveError`.
111
+
112
+ ## Webhooks
113
+
114
+ Pass the **raw** body, exactly as received:
115
+
116
+ ```python
117
+ from mailhive import WebhookVerificationError, verify_webhook
118
+
119
+ # Django
120
+ def mailhive_webhook(request):
121
+ try:
122
+ event = verify_webhook(request.body, request.headers.get("Mailhive-Signature"), settings.MAILHIVE_WEBHOOK_SECRET)
123
+ except WebhookVerificationError:
124
+ return HttpResponse(status=400)
125
+ ...
126
+
127
+ # Flask: verify_webhook(request.get_data(), request.headers.get("Mailhive-Signature"), secret)
128
+ # FastAPI: verify_webhook(await request.body(), request.headers.get("mailhive-signature"), secret)
129
+ ```
130
+
131
+ ## Django
132
+
133
+ ```python
134
+ # settings.py, Django 6.1 and later
135
+ MAILERS = {
136
+ "default": {
137
+ "BACKEND": "mailhive.django.EmailBackend",
138
+ "OPTIONS": {"api_key": os.environ["MAILHIVE_API_KEY"]},
139
+ },
140
+ }
141
+
142
+ # settings.py, earlier versions
143
+ EMAIL_BACKEND = "mailhive.django.EmailBackend"
144
+ MAILHIVE_API_KEY = os.environ["MAILHIVE_API_KEY"]
145
+ ```
146
+
147
+ `send_mail`, `EmailMessage` and `EmailMultiAlternatives` all send through Mailhive, including HTML alternatives, attachments, cc, bcc, Reply-To and extra headers. You can also set `message.mailhive_tags` or `message.mailhive_idempotency_key` on a message.
148
+
149
+ ## Options
150
+
151
+ ```python
152
+ Mailhive(
153
+ api_key="mhs_…", # default: MAILHIVE_API_KEY
154
+ base_url="https://api-beta.mailhive.africa/v1", # default: MAILHIVE_BASE_URL, then production
155
+ timeout=30.0,
156
+ max_retries=2,
157
+ )
158
+ ```
159
+
160
+ **Test keys** (`mhs_test_…`) work unchanged: delivery is simulated and nothing is billed. [More about test keys](https://mailhive.africa/docs/send/api-keys#test-keys).
@@ -0,0 +1,136 @@
1
+ # mailhive
2
+
3
+ The official Python SDK for [Mailhive Send](https://mailhive.africa/docs/send/overview). It supports Python 3.9+, with sync and async clients and a Django email backend.
4
+
5
+ ```sh
6
+ pip install mailhive
7
+ ```
8
+
9
+ ## Send an email
10
+
11
+ ```python
12
+ from mailhive import Mailhive
13
+
14
+ client = Mailhive() # reads MAILHIVE_API_KEY
15
+
16
+ email = client.emails.send({
17
+ "from": "Acme <hello@acme.com>",
18
+ "to": "ada@example.com",
19
+ "subject": "Your receipt",
20
+ "html": "<p>Thanks for your order.</p>",
21
+ })
22
+ print(email["id"])
23
+ ```
24
+
25
+ The keys match the [API reference](https://mailhive.africa/docs/api/send-email) exactly: `cc`, `bcc`, `reply_to`, `headers`, `tags`, `template_id`, `variables` and `attachments`. You can also pass keyword arguments, with `from_` standing in for `from`:
26
+
27
+ ```python
28
+ client.emails.send(from_="hello@acme.com", to="ada@example.com", subject="Hi", text="Hello")
29
+ ```
30
+
31
+ Attachment `content` can be `bytes`, which are encoded for you, or base64 text.
32
+
33
+ ```python
34
+ client.emails.send_batch([email1, email2]) # up to 100; all accepted or none
35
+ client.emails.get(email_id) # status: "delivered", "bounced", …
36
+ ```
37
+
38
+ ### Async
39
+
40
+ ```python
41
+ from mailhive import AsyncMailhive
42
+
43
+ async with AsyncMailhive() as client:
44
+ await client.emails.send({...})
45
+ ```
46
+
47
+ ## Retries and idempotency
48
+
49
+ Every send carries an `Idempotency-Key`. Retries reuse it, so **a retry never sends twice**.
50
+
51
+ - **Retried** (up to 2 times, then configurable): network errors, timeouts, 5xx responses, and `429 rate_limited` after the `Retry-After` delay.
52
+ - **Not retried:** a used-up allowance (`monthly_quota_reached`, `daily_cap_reached`) and invalid requests.
53
+
54
+ To stay safe across restarts, pass your own key:
55
+
56
+ ```python
57
+ client.emails.send(email, idempotency_key=f"order-{order.id}-receipt")
58
+ ```
59
+
60
+ ## Errors
61
+
62
+ ```python
63
+ from mailhive import RateLimitError, ValidationError
64
+
65
+ try:
66
+ client.emails.send(email)
67
+ except ValidationError as error:
68
+ print(error.code, error.message, error.details, error.request_id)
69
+ except RateLimitError as error:
70
+ if error.code == "monthly_quota_reached":
71
+ ...
72
+ ```
73
+
74
+ | Exception | Status |
75
+ |---|---|
76
+ | `AuthenticationError` | 401 |
77
+ | `BillingError` | 402 |
78
+ | `PermissionDeniedError` | 403 |
79
+ | `NotFoundError` | 404 |
80
+ | `ConflictError` | 409 (e.g. `idempotency_conflict`) |
81
+ | `ValidationError` | 422 |
82
+ | `RateLimitError` | 429 (`retry_after` in seconds) |
83
+ | `APIError` | other statuses, and the base class of all of the above |
84
+ | `APIConnectionError` | no response |
85
+
86
+ All of them subclass `MailhiveError`.
87
+
88
+ ## Webhooks
89
+
90
+ Pass the **raw** body, exactly as received:
91
+
92
+ ```python
93
+ from mailhive import WebhookVerificationError, verify_webhook
94
+
95
+ # Django
96
+ def mailhive_webhook(request):
97
+ try:
98
+ event = verify_webhook(request.body, request.headers.get("Mailhive-Signature"), settings.MAILHIVE_WEBHOOK_SECRET)
99
+ except WebhookVerificationError:
100
+ return HttpResponse(status=400)
101
+ ...
102
+
103
+ # Flask: verify_webhook(request.get_data(), request.headers.get("Mailhive-Signature"), secret)
104
+ # FastAPI: verify_webhook(await request.body(), request.headers.get("mailhive-signature"), secret)
105
+ ```
106
+
107
+ ## Django
108
+
109
+ ```python
110
+ # settings.py, Django 6.1 and later
111
+ MAILERS = {
112
+ "default": {
113
+ "BACKEND": "mailhive.django.EmailBackend",
114
+ "OPTIONS": {"api_key": os.environ["MAILHIVE_API_KEY"]},
115
+ },
116
+ }
117
+
118
+ # settings.py, earlier versions
119
+ EMAIL_BACKEND = "mailhive.django.EmailBackend"
120
+ MAILHIVE_API_KEY = os.environ["MAILHIVE_API_KEY"]
121
+ ```
122
+
123
+ `send_mail`, `EmailMessage` and `EmailMultiAlternatives` all send through Mailhive, including HTML alternatives, attachments, cc, bcc, Reply-To and extra headers. You can also set `message.mailhive_tags` or `message.mailhive_idempotency_key` on a message.
124
+
125
+ ## Options
126
+
127
+ ```python
128
+ Mailhive(
129
+ api_key="mhs_…", # default: MAILHIVE_API_KEY
130
+ base_url="https://api-beta.mailhive.africa/v1", # default: MAILHIVE_BASE_URL, then production
131
+ timeout=30.0,
132
+ max_retries=2,
133
+ )
134
+ ```
135
+
136
+ **Test keys** (`mhs_test_…`) work unchanged: delivery is simulated and nothing is billed. [More about test keys](https://mailhive.africa/docs/send/api-keys#test-keys).
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.24"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mailhive"
7
+ dynamic = ["version"]
8
+ description = "The official Python SDK for Mailhive Send: transactional email from Africa."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.9"
13
+ authors = [{ name = "Naszat" }]
14
+ keywords = ["mailhive", "email", "transactional-email", "django", "africa"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Framework :: Django",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Communications :: Email",
21
+ "Typing :: Typed",
22
+ ]
23
+ dependencies = ["httpx>=0.25,<1", "typing-extensions>=4.7; python_version<'3.11'"]
24
+
25
+ [project.optional-dependencies]
26
+ django = ["django>=4.2"]
27
+
28
+ [project.urls]
29
+ Homepage = "https://mailhive.africa/docs/sdks/python"
30
+ Source = "https://github.com/Naszat/mailhive-sdks/tree/main/python"
31
+ Issues = "https://github.com/Naszat/mailhive-sdks/issues"
32
+
33
+ [tool.hatch.version]
34
+ path = "src/mailhive/_version.py"
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/mailhive"]
38
+
39
+ [tool.pytest.ini_options]
40
+ testpaths = ["tests"]
41
+ pythonpath = ["."]
@@ -0,0 +1,48 @@
1
+ """The official Python SDK for Mailhive Send.
2
+
3
+ from mailhive import Mailhive
4
+
5
+ client = Mailhive() # reads MAILHIVE_API_KEY
6
+ client.emails.send({"from": "hello@acme.com", "to": "ada@example.com", "subject": "Hi", "text": "Hello"})
7
+ """
8
+
9
+ from ._client import DEFAULT_BASE_URL, AsyncMailhive, Mailhive
10
+ from ._errors import (
11
+ APIConnectionError,
12
+ APIError,
13
+ AuthenticationError,
14
+ BillingError,
15
+ ConflictError,
16
+ MailhiveError,
17
+ NotFoundError,
18
+ PermissionDeniedError,
19
+ RateLimitError,
20
+ ValidationError,
21
+ WebhookVerificationError,
22
+ )
23
+ from ._types import AcceptedEmail, Attachment, Email, SendEmailParams
24
+ from ._version import __version__
25
+ from ._webhooks import verify as verify_webhook
26
+
27
+ __all__ = [
28
+ "DEFAULT_BASE_URL",
29
+ "APIConnectionError",
30
+ "APIError",
31
+ "AcceptedEmail",
32
+ "AsyncMailhive",
33
+ "Attachment",
34
+ "AuthenticationError",
35
+ "BillingError",
36
+ "ConflictError",
37
+ "Email",
38
+ "Mailhive",
39
+ "MailhiveError",
40
+ "NotFoundError",
41
+ "PermissionDeniedError",
42
+ "RateLimitError",
43
+ "SendEmailParams",
44
+ "ValidationError",
45
+ "WebhookVerificationError",
46
+ "__version__",
47
+ "verify_webhook",
48
+ ]