notifyvia-email 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,149 @@
1
+ Metadata-Version: 2.3
2
+ Name: notifyvia-email
3
+ Version: 0.1.0
4
+ Summary: The frontend Python library for notifyvia.email
5
+ Requires-Dist: requests>=2.34.2
6
+ Requires-Python: >=3.13
7
+ Description-Content-Type: text/markdown
8
+
9
+ # notifyvia-email
10
+
11
+ The frontend Python library for [notifyvia.email](https://notifyvia.email/docs).
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pip install notifyvia-email
17
+ ```
18
+
19
+ or with uv:
20
+
21
+ ```bash
22
+ uv add notifyvia-email
23
+ ```
24
+
25
+ Requires Python 3.13+.
26
+
27
+ ## Quick start
28
+
29
+ ```python
30
+ from notifyvia_email.v2 import NotifyViaEmail, SendResult
31
+
32
+ client = NotifyViaEmail(api_key="your-api-key")
33
+
34
+ resp = client.send(
35
+ to="you@example.com",
36
+ subject="A message from your website",
37
+ text="Do you have any strawberry cheesecake?",
38
+ )
39
+
40
+ if resp.ok:
41
+ print("Email queued for delivery")
42
+ else:
43
+ print(resp.result, resp.errors)
44
+ ```
45
+
46
+ ## API versions
47
+
48
+ Each API version has its own client with the same interface:
49
+
50
+ ```python
51
+ from notifyvia_email.v1 import NotifyViaEmail # POST /api/v1/send
52
+ from notifyvia_email.v2 import NotifyViaEmail # POST /api/v2/send
53
+ ```
54
+
55
+ v2 adds the `reply_to` parameter. Everything else is shared.
56
+
57
+ ## Sending HTML
58
+
59
+ Provide `html`, `text`, or both. At least one is required, otherwise `send()` raises `ValueError`.
60
+
61
+ ```python
62
+ resp = client.send(
63
+ to="you@example.com",
64
+ subject="Welcome!",
65
+ html="<h1>Welcome</h1><p>Thanks for signing up.</p>",
66
+ text="Welcome! Thanks for signing up.",
67
+ reply_to="support@example.com", # v2 only
68
+ )
69
+ ```
70
+
71
+ ## Spam checking and rate limiting
72
+
73
+ A typical contact form setup. The rate limiter needs the sender's IPv4 address.
74
+
75
+ ```python
76
+ resp = client.send(
77
+ to="owner@example.com",
78
+ subject="New contact form submission",
79
+ text=form_message,
80
+ enable_spam_checker=True,
81
+ enable_rate_limiter=True,
82
+ limit=3, # max requests...
83
+ within_seconds=300, # ...per 5 minutes, per IP
84
+ ipv4_address=request.remote_addr,
85
+ )
86
+ ```
87
+
88
+ ## Choosing a service
89
+
90
+ ```python
91
+ resp = client.send(
92
+ to="you@example.com",
93
+ subject="Hello",
94
+ text="Sent via Postmark",
95
+ service="postmark", # "zepto" (default) or "postmark"
96
+ )
97
+ ```
98
+
99
+ ## Handling the response
100
+
101
+ `send()` returns a `SendResponse` with:
102
+
103
+ - `result`: a `SendResult` enum member
104
+ - `errors`: the API's `errors` field (or `None`), e.g. per-field validation messages
105
+ - `ok`: `True` when the email was accepted for delivery
106
+
107
+ ```python
108
+ from notifyvia_email.v2 import SendResult
109
+
110
+ resp = client.send(to="not-an-email", subject="Hi", text="Hello")
111
+
112
+ match resp.result:
113
+ case SendResult.EMAIL_PROVISIONING:
114
+ print("Sent")
115
+ case SendResult.INVALID:
116
+ print("Validation failed:", resp.errors)
117
+ # {'to': ['Not a valid email address.']}
118
+ case SendResult.RATE_LIMIT_EXCEEDED:
119
+ print("Too many requests, try again later")
120
+ case SendResult.SPAM_DETECTED:
121
+ print("Message rejected as spam")
122
+ case SendResult.CONNECTION_ERROR:
123
+ print("Could not reach notifyvia.email:", resp.errors)
124
+ case _:
125
+ print("Failed:", resp.result.name, resp.errors)
126
+ ```
127
+
128
+ | `SendResult` | HTTP | Meaning |
129
+ |-----------------------------|------|--------------------------------------------------|
130
+ | `EMAIL_PROVISIONING` | 200 | Accepted and queued for delivery |
131
+ | `AUTHORIZATION_REQUIRED` | 401 | Missing or invalid API key |
132
+ | `RATE_LIMIT_EXCEEDED` | 429 | Too many requests from this IP in the window |
133
+ | `INVALID` | 460 | Request validation failed (see `errors`) |
134
+ | `INVALID_ACCOUNT` | 461 | API key accepted but no matching account |
135
+ | `INVALID_SERVICE` | 462 | `service` is not a supported provider |
136
+ | `INSUFFICIENT_CREDITS` | 463 | Not enough credits to send |
137
+ | `IPV4_ADDRESS_NOT_PROVIDED` | 464 | Rate limiter enabled without `ipv4_address` |
138
+ | `SPAM_DETECTED` | 465 | Body matched the spam dictionary |
139
+ | `SENDER_IPV4_BLOCKED` | 466 | Sender IP is on the global block list |
140
+ | `UNKNOWN_ERROR` | - | Unexpected status code from the API |
141
+ | `CONNECTION_ERROR` | - | Network failure or timeout; `errors` has details |
142
+
143
+ ## Configuration
144
+
145
+ ```python
146
+ client = NotifyViaEmail(api_key="your-api-key", timeout=5.0) # seconds, default 10
147
+ ```
148
+
149
+ See the [notifyvia.email docs](https://notifyvia.email/docs) for full API details.
@@ -0,0 +1,141 @@
1
+ # notifyvia-email
2
+
3
+ The frontend Python library for [notifyvia.email](https://notifyvia.email/docs).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install notifyvia-email
9
+ ```
10
+
11
+ or with uv:
12
+
13
+ ```bash
14
+ uv add notifyvia-email
15
+ ```
16
+
17
+ Requires Python 3.13+.
18
+
19
+ ## Quick start
20
+
21
+ ```python
22
+ from notifyvia_email.v2 import NotifyViaEmail, SendResult
23
+
24
+ client = NotifyViaEmail(api_key="your-api-key")
25
+
26
+ resp = client.send(
27
+ to="you@example.com",
28
+ subject="A message from your website",
29
+ text="Do you have any strawberry cheesecake?",
30
+ )
31
+
32
+ if resp.ok:
33
+ print("Email queued for delivery")
34
+ else:
35
+ print(resp.result, resp.errors)
36
+ ```
37
+
38
+ ## API versions
39
+
40
+ Each API version has its own client with the same interface:
41
+
42
+ ```python
43
+ from notifyvia_email.v1 import NotifyViaEmail # POST /api/v1/send
44
+ from notifyvia_email.v2 import NotifyViaEmail # POST /api/v2/send
45
+ ```
46
+
47
+ v2 adds the `reply_to` parameter. Everything else is shared.
48
+
49
+ ## Sending HTML
50
+
51
+ Provide `html`, `text`, or both. At least one is required, otherwise `send()` raises `ValueError`.
52
+
53
+ ```python
54
+ resp = client.send(
55
+ to="you@example.com",
56
+ subject="Welcome!",
57
+ html="<h1>Welcome</h1><p>Thanks for signing up.</p>",
58
+ text="Welcome! Thanks for signing up.",
59
+ reply_to="support@example.com", # v2 only
60
+ )
61
+ ```
62
+
63
+ ## Spam checking and rate limiting
64
+
65
+ A typical contact form setup. The rate limiter needs the sender's IPv4 address.
66
+
67
+ ```python
68
+ resp = client.send(
69
+ to="owner@example.com",
70
+ subject="New contact form submission",
71
+ text=form_message,
72
+ enable_spam_checker=True,
73
+ enable_rate_limiter=True,
74
+ limit=3, # max requests...
75
+ within_seconds=300, # ...per 5 minutes, per IP
76
+ ipv4_address=request.remote_addr,
77
+ )
78
+ ```
79
+
80
+ ## Choosing a service
81
+
82
+ ```python
83
+ resp = client.send(
84
+ to="you@example.com",
85
+ subject="Hello",
86
+ text="Sent via Postmark",
87
+ service="postmark", # "zepto" (default) or "postmark"
88
+ )
89
+ ```
90
+
91
+ ## Handling the response
92
+
93
+ `send()` returns a `SendResponse` with:
94
+
95
+ - `result`: a `SendResult` enum member
96
+ - `errors`: the API's `errors` field (or `None`), e.g. per-field validation messages
97
+ - `ok`: `True` when the email was accepted for delivery
98
+
99
+ ```python
100
+ from notifyvia_email.v2 import SendResult
101
+
102
+ resp = client.send(to="not-an-email", subject="Hi", text="Hello")
103
+
104
+ match resp.result:
105
+ case SendResult.EMAIL_PROVISIONING:
106
+ print("Sent")
107
+ case SendResult.INVALID:
108
+ print("Validation failed:", resp.errors)
109
+ # {'to': ['Not a valid email address.']}
110
+ case SendResult.RATE_LIMIT_EXCEEDED:
111
+ print("Too many requests, try again later")
112
+ case SendResult.SPAM_DETECTED:
113
+ print("Message rejected as spam")
114
+ case SendResult.CONNECTION_ERROR:
115
+ print("Could not reach notifyvia.email:", resp.errors)
116
+ case _:
117
+ print("Failed:", resp.result.name, resp.errors)
118
+ ```
119
+
120
+ | `SendResult` | HTTP | Meaning |
121
+ |-----------------------------|------|--------------------------------------------------|
122
+ | `EMAIL_PROVISIONING` | 200 | Accepted and queued for delivery |
123
+ | `AUTHORIZATION_REQUIRED` | 401 | Missing or invalid API key |
124
+ | `RATE_LIMIT_EXCEEDED` | 429 | Too many requests from this IP in the window |
125
+ | `INVALID` | 460 | Request validation failed (see `errors`) |
126
+ | `INVALID_ACCOUNT` | 461 | API key accepted but no matching account |
127
+ | `INVALID_SERVICE` | 462 | `service` is not a supported provider |
128
+ | `INSUFFICIENT_CREDITS` | 463 | Not enough credits to send |
129
+ | `IPV4_ADDRESS_NOT_PROVIDED` | 464 | Rate limiter enabled without `ipv4_address` |
130
+ | `SPAM_DETECTED` | 465 | Body matched the spam dictionary |
131
+ | `SENDER_IPV4_BLOCKED` | 466 | Sender IP is on the global block list |
132
+ | `UNKNOWN_ERROR` | - | Unexpected status code from the API |
133
+ | `CONNECTION_ERROR` | - | Network failure or timeout; `errors` has details |
134
+
135
+ ## Configuration
136
+
137
+ ```python
138
+ client = NotifyViaEmail(api_key="your-api-key", timeout=5.0) # seconds, default 10
139
+ ```
140
+
141
+ See the [notifyvia.email docs](https://notifyvia.email/docs) for full API details.
@@ -0,0 +1,20 @@
1
+ [project]
2
+ name = "notifyvia-email"
3
+ version = "0.1.0"
4
+ description = "The frontend Python library for notifyvia.email"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = ["requests>=2.34.2"]
8
+
9
+ [dependency-groups]
10
+ dev = ["pyqwe>=3.2.0"]
11
+
12
+ [tool.pyqwe]
13
+ sync = "*:uv run --env-file .index-notifyviaemail -- uv sync"
14
+ build = "*:uv build"
15
+ publish = "*:uv run --env-file .index-notifyviaemail -- uv publish --index uilix-forgejo"
16
+ uv = "*>:uv run --env-file .index-notifyviaemail -- uv"
17
+
18
+ [build-system]
19
+ requires = ["uv_build>=0.8.0,<0.12"]
20
+ build-backend = "uv_build"
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "notifyvia-email"
3
+ version = "0.1.0"
4
+ description = "The frontend Python library for notifyvia.email"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = [
8
+ "requests>=2.34.2",
9
+ ]
10
+
11
+ [dependency-groups]
12
+ dev = [
13
+ "pyqwe>=3.2.0",
14
+ ]
15
+
16
+ [tool.pyqwe]
17
+ sync = "*:uv run --env-file .index-notifyviaemail -- uv sync"
18
+ build = "*:uv build"
19
+ publish = "*:uv run --env-file .index-notifyviaemail -- uv publish --index uilix-forgejo"
20
+ uv = "*>:uv run --env-file .index-notifyviaemail -- uv"
21
+
22
+ [build-system]
23
+ requires = ["uv_build>=0.8.0,<0.12"]
24
+ build-backend = "uv_build"
File without changes
@@ -0,0 +1,112 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+ from typing import Any, Literal
4
+
5
+ import requests
6
+
7
+ BASE_URL = "https://notifyvia.email/api"
8
+
9
+ Service = Literal["zepto", "postmark"]
10
+
11
+
12
+ class SendResult(Enum):
13
+ """Outcome of a send request, mapped from the API's HTTP status code."""
14
+
15
+ EMAIL_PROVISIONING = 200
16
+ RATE_LIMIT_EXCEEDED = 429
17
+ AUTHORIZATION_REQUIRED = 401
18
+ INVALID = 460
19
+ INVALID_ACCOUNT = 461
20
+ INVALID_SERVICE = 462
21
+ INSUFFICIENT_CREDITS = 463
22
+ IPV4_ADDRESS_NOT_PROVIDED = 464
23
+ SPAM_DETECTED = 465
24
+ SENDER_IPV4_BLOCKED = 466
25
+ UNKNOWN_ERROR = -1
26
+ CONNECTION_ERROR = -2
27
+
28
+ @property
29
+ def ok(self) -> bool:
30
+ return self is SendResult.EMAIL_PROVISIONING
31
+
32
+ @classmethod
33
+ def from_status_code(cls, status_code: int) -> "SendResult":
34
+ try:
35
+ return cls(status_code)
36
+ except ValueError:
37
+ return cls.UNKNOWN_ERROR
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class SendResponse:
42
+ """The result enum plus the API's ``errors`` field (e.g. per-field messages on INVALID)."""
43
+
44
+ result: SendResult
45
+ errors: Any = None
46
+
47
+ @property
48
+ def ok(self) -> bool:
49
+ return self.result.ok
50
+
51
+
52
+ class BaseNotifyViaEmail:
53
+ version: str
54
+
55
+ def __init__(self, api_key: str, timeout: float = 10.0):
56
+ self.api_key = api_key
57
+ self.timeout = timeout
58
+
59
+ @property
60
+ def url(self) -> str:
61
+ return f"{BASE_URL}/{self.version}/send"
62
+
63
+ def _post(self, payload: dict[str, Any]) -> SendResponse:
64
+ try:
65
+ response = requests.post(
66
+ self.url,
67
+ json=payload,
68
+ headers={"X-Api-Key": self.api_key},
69
+ timeout=self.timeout,
70
+ )
71
+ except requests.RequestException as e:
72
+ return SendResponse(SendResult.CONNECTION_ERROR, errors=str(e))
73
+
74
+ try:
75
+ errors = response.json().get("errors")
76
+ except (ValueError, AttributeError):
77
+ errors = None
78
+
79
+ return SendResponse(SendResult.from_status_code(response.status_code), errors)
80
+
81
+ @staticmethod
82
+ def _build_payload(
83
+ to: str,
84
+ subject: str,
85
+ html: str | None,
86
+ text: str | None,
87
+ enable_spam_checker: bool,
88
+ enable_rate_limiter: bool,
89
+ limit: int,
90
+ within_seconds: int,
91
+ ipv4_address: str | None,
92
+ service: Service,
93
+ ) -> dict[str, Any]:
94
+ if html is None and text is None:
95
+ raise ValueError("Either html or text must be provided")
96
+
97
+ payload: dict[str, Any] = {
98
+ "to": to,
99
+ "subject": subject,
100
+ "enable_spam_checker": enable_spam_checker,
101
+ "enable_rate_limiter": enable_rate_limiter,
102
+ "limit": limit,
103
+ "within_seconds": within_seconds,
104
+ "service": service,
105
+ }
106
+ if html is not None:
107
+ payload["html"] = html
108
+ if text is not None:
109
+ payload["text"] = text
110
+ if ipv4_address is not None:
111
+ payload["ipv4_address"] = ipv4_address
112
+ return payload
@@ -0,0 +1,36 @@
1
+ from .._base import BaseNotifyViaEmail, SendResponse, SendResult, Service
2
+
3
+ __all__ = ["NotifyViaEmail", "SendResponse", "SendResult"]
4
+
5
+
6
+ class NotifyViaEmail(BaseNotifyViaEmail):
7
+ """Client for POST https://notifyvia.email/api/v1/send"""
8
+
9
+ version = "v1"
10
+
11
+ def send(
12
+ self,
13
+ to: str,
14
+ subject: str,
15
+ html: str | None = None,
16
+ text: str | None = None,
17
+ enable_spam_checker: bool = False,
18
+ enable_rate_limiter: bool = False,
19
+ limit: int = 3,
20
+ within_seconds: int = 300,
21
+ ipv4_address: str | None = None,
22
+ service: Service = "zepto",
23
+ ) -> SendResponse:
24
+ payload = self._build_payload(
25
+ to=to,
26
+ subject=subject,
27
+ html=html,
28
+ text=text,
29
+ enable_spam_checker=enable_spam_checker,
30
+ enable_rate_limiter=enable_rate_limiter,
31
+ limit=limit,
32
+ within_seconds=within_seconds,
33
+ ipv4_address=ipv4_address,
34
+ service=service,
35
+ )
36
+ return self._post(payload)
@@ -0,0 +1,39 @@
1
+ from .._base import BaseNotifyViaEmail, SendResponse, SendResult, Service
2
+
3
+ __all__ = ["NotifyViaEmail", "SendResponse", "SendResult"]
4
+
5
+
6
+ class NotifyViaEmail(BaseNotifyViaEmail):
7
+ """Client for POST https://notifyvia.email/api/v2/send"""
8
+
9
+ version = "v2"
10
+
11
+ def send(
12
+ self,
13
+ to: str,
14
+ subject: str,
15
+ html: str | None = None,
16
+ text: str | None = None,
17
+ reply_to: str | None = None,
18
+ enable_spam_checker: bool = False,
19
+ enable_rate_limiter: bool = False,
20
+ limit: int = 3,
21
+ within_seconds: int = 300,
22
+ ipv4_address: str | None = None,
23
+ service: Service = "zepto",
24
+ ) -> SendResponse:
25
+ payload = self._build_payload(
26
+ to=to,
27
+ subject=subject,
28
+ html=html,
29
+ text=text,
30
+ enable_spam_checker=enable_spam_checker,
31
+ enable_rate_limiter=enable_rate_limiter,
32
+ limit=limit,
33
+ within_seconds=within_seconds,
34
+ ipv4_address=ipv4_address,
35
+ service=service,
36
+ )
37
+ if reply_to is not None:
38
+ payload["reply_to"] = reply_to
39
+ return self._post(payload)