sendgrid-async 2.2.0__tar.gz → 2.3.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.
- {sendgrid_async-2.2.0 → sendgrid_async-2.3.0}/PKG-INFO +12 -7
- {sendgrid_async-2.2.0 → sendgrid_async-2.3.0}/README.md +11 -6
- sendgrid_async-2.3.0/async_sendgrid/__init__.py +13 -0
- {sendgrid_async-2.2.0 → sendgrid_async-2.3.0}/async_sendgrid/pool.py +73 -20
- {sendgrid_async-2.2.0 → sendgrid_async-2.3.0}/async_sendgrid/sendgrid.py +31 -9
- {sendgrid_async-2.2.0 → sendgrid_async-2.3.0}/pyproject.toml +1 -1
- sendgrid_async-2.2.0/async_sendgrid/__init__.py +0 -4
- {sendgrid_async-2.2.0 → sendgrid_async-2.3.0}/LICENSE +0 -0
- {sendgrid_async-2.2.0 → sendgrid_async-2.3.0}/async_sendgrid/exception.py +0 -0
- {sendgrid_async-2.2.0 → sendgrid_async-2.3.0}/async_sendgrid/telemetry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: sendgrid-async
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: SendGrid using an httpx client
|
|
5
5
|
Home-page: https://github.com/EM51641/async-sendgrid-
|
|
6
6
|
License: MIT
|
|
@@ -131,17 +131,22 @@ To disable retries entirely, set `retry_attempts=0`:
|
|
|
131
131
|
pool = ConnectionPool(retry_attempts=0)
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
### Shutdown
|
|
135
|
+
|
|
136
|
+
When your application is shutting down, call `shutdown()` on the pool to close all connections and release resources:
|
|
135
137
|
|
|
136
138
|
```python
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
pool = ConnectionPool()
|
|
140
|
+
sendgrid = SendgridAPI(api_key="YOUR_API_KEY", pool=pool)
|
|
141
|
+
|
|
142
|
+
# ... send emails ...
|
|
139
143
|
|
|
140
|
-
#
|
|
141
|
-
|
|
142
|
-
response = await sendgrid.send(email, backoff=0.1)
|
|
144
|
+
# On application shutdown
|
|
145
|
+
await pool.shutdown()
|
|
143
146
|
```
|
|
144
147
|
|
|
148
|
+
> **Note:** Per-call `retry` or `backoff` overrides on `send()` create and tear down an ephemeral connection (TCP + TLS handshake) for every request. Because an email send is a lightweight operation — a small JSON payload answered with a `202` — the connection overhead can easily exceed the request itself. Configure retry and backoff on the `ConnectionPool` at initialization instead.
|
|
149
|
+
|
|
145
150
|
### Send emails on behalf of another user
|
|
146
151
|
|
|
147
152
|
Send emails on behalf of subusers:
|
|
@@ -102,17 +102,22 @@ To disable retries entirely, set `retry_attempts=0`:
|
|
|
102
102
|
pool = ConnectionPool(retry_attempts=0)
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
### Shutdown
|
|
106
|
+
|
|
107
|
+
When your application is shutting down, call `shutdown()` on the pool to close all connections and release resources:
|
|
106
108
|
|
|
107
109
|
```python
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
pool = ConnectionPool()
|
|
111
|
+
sendgrid = SendgridAPI(api_key="YOUR_API_KEY", pool=pool)
|
|
112
|
+
|
|
113
|
+
# ... send emails ...
|
|
110
114
|
|
|
111
|
-
#
|
|
112
|
-
|
|
113
|
-
response = await sendgrid.send(email, backoff=0.1)
|
|
115
|
+
# On application shutdown
|
|
116
|
+
await pool.shutdown()
|
|
114
117
|
```
|
|
115
118
|
|
|
119
|
+
> **Note:** Per-call `retry` or `backoff` overrides on `send()` create and tear down an ephemeral connection (TCP + TLS handshake) for every request. Because an email send is a lightweight operation — a small JSON payload answered with a `202` — the connection overhead can easily exceed the request itself. Configure retry and backoff on the `ConnectionPool` at initialization instead.
|
|
120
|
+
|
|
116
121
|
### Send emails on behalf of another user
|
|
117
122
|
|
|
118
123
|
Send emails on behalf of subusers:
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
|
|
3
|
+
from .sendgrid import SendgridAPI # noqa
|
|
4
|
+
from .pool import ConnectionPool # noqa
|
|
5
|
+
|
|
6
|
+
__version__ = "0.0.0-dev"
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
__version__ = version("sendgrid-async")
|
|
10
|
+
except PackageNotFoundError:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
__all__ = ["SendgridAPI", "ConnectionPool"]
|
|
@@ -6,7 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
from typing import TYPE_CHECKING
|
|
8
8
|
|
|
9
|
-
from httpx import AsyncClient, Limits # type: ignore
|
|
9
|
+
from httpx import AsyncClient, AsyncHTTPTransport, Limits # type: ignore
|
|
10
10
|
from httpx_retries import Retry, RetryTransport # type: ignore
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
@@ -27,6 +27,7 @@ class ConnectionPool:
|
|
|
27
27
|
retry_attempts: int = 5,
|
|
28
28
|
backoff_factor: float = 0.5,
|
|
29
29
|
backoff_jitter: float = 1.0,
|
|
30
|
+
timeout: float = 5.0,
|
|
30
31
|
) -> None:
|
|
31
32
|
"""
|
|
32
33
|
Initialize the connection pool.
|
|
@@ -51,10 +52,13 @@ class ConnectionPool:
|
|
|
51
52
|
backoff_jitter (float, optional):
|
|
52
53
|
Jitter multiplier applied to the backoff time,
|
|
53
54
|
between 0 and 1. Defaults to 1.0.
|
|
55
|
+
timeout (float, optional):
|
|
56
|
+
Request timeout in seconds. Defaults to 5.0.
|
|
54
57
|
"""
|
|
55
58
|
self._validate_retry_attempts(retry_attempts)
|
|
56
59
|
self._validate_backoff_factor(backoff_factor)
|
|
57
60
|
self._validate_backoff_jitter(backoff_jitter)
|
|
61
|
+
self._validate_timeout(timeout)
|
|
58
62
|
|
|
59
63
|
self._limits = Limits(
|
|
60
64
|
max_connections=max_connections,
|
|
@@ -67,16 +71,44 @@ class ConnectionPool:
|
|
|
67
71
|
backoff_jitter=backoff_jitter,
|
|
68
72
|
allowed_methods=["POST"],
|
|
69
73
|
)
|
|
74
|
+
self._timeout = timeout
|
|
70
75
|
self._client: AsyncClient | None = None
|
|
76
|
+
self._shutdown = False
|
|
71
77
|
|
|
72
|
-
def _create_client(
|
|
78
|
+
def _create_client(self, headers: dict[str, Any]) -> AsyncClient:
|
|
79
|
+
"""
|
|
80
|
+
Get or create the long-lived HTTP client backed by the pool.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
headers (dict[str, Any]): The headers to use for the client.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
AsyncClient: The configured HTTP client.
|
|
87
|
+
"""
|
|
88
|
+
if self._client is not None and not self._client.is_closed:
|
|
89
|
+
return self._client
|
|
90
|
+
|
|
91
|
+
transport = RetryTransport(
|
|
92
|
+
transport=AsyncHTTPTransport(limits=self._limits),
|
|
93
|
+
retry=self._retry,
|
|
94
|
+
)
|
|
95
|
+
self._client = AsyncClient(
|
|
96
|
+
headers=headers,
|
|
97
|
+
timeout=self._timeout,
|
|
98
|
+
transport=transport,
|
|
99
|
+
)
|
|
100
|
+
return self._client
|
|
101
|
+
|
|
102
|
+
def _create_ephemeral_client(
|
|
73
103
|
self,
|
|
74
104
|
headers: dict[str, Any],
|
|
75
105
|
retry: int | None = None,
|
|
76
106
|
backoff: float | None = None,
|
|
77
107
|
) -> AsyncClient:
|
|
78
108
|
"""
|
|
79
|
-
|
|
109
|
+
Create a single-use HTTP client with custom retry/backoff settings.
|
|
110
|
+
|
|
111
|
+
The caller is responsible for closing the returned client.
|
|
80
112
|
|
|
81
113
|
Args:
|
|
82
114
|
headers (dict[str, Any]): The headers to use for the client.
|
|
@@ -86,28 +118,43 @@ class ConnectionPool:
|
|
|
86
118
|
Uses the pool default when not set.
|
|
87
119
|
|
|
88
120
|
Returns:
|
|
89
|
-
AsyncClient:
|
|
121
|
+
AsyncClient: An ephemeral HTTP client.
|
|
90
122
|
"""
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
123
|
+
if retry is not None:
|
|
124
|
+
self._validate_retry_attempts(retry)
|
|
125
|
+
if backoff is not None:
|
|
126
|
+
self._validate_backoff_factor(backoff)
|
|
127
|
+
|
|
128
|
+
retry_strategy = Retry(
|
|
129
|
+
total=retry if retry is not None else self._retry.total,
|
|
130
|
+
backoff_factor=(
|
|
131
|
+
backoff if backoff is not None else self._retry.backoff_factor
|
|
132
|
+
),
|
|
133
|
+
backoff_jitter=self._retry.backoff_jitter,
|
|
134
|
+
allowed_methods=["POST"],
|
|
135
|
+
)
|
|
136
|
+
transport = RetryTransport(
|
|
137
|
+
transport=AsyncHTTPTransport(),
|
|
138
|
+
retry=retry_strategy,
|
|
139
|
+
)
|
|
104
140
|
return AsyncClient(
|
|
105
141
|
headers=headers,
|
|
106
|
-
|
|
107
|
-
timeout=5.0,
|
|
142
|
+
timeout=self._timeout,
|
|
108
143
|
transport=transport,
|
|
109
144
|
)
|
|
110
145
|
|
|
146
|
+
@property
|
|
147
|
+
def is_shutdown(self) -> bool:
|
|
148
|
+
"""Whether the pool has been explicitly shut down."""
|
|
149
|
+
return self._shutdown
|
|
150
|
+
|
|
151
|
+
async def shutdown(self) -> None:
|
|
152
|
+
"""Close all clients and release their connections."""
|
|
153
|
+
self._shutdown = True
|
|
154
|
+
if self._client is not None and not self._client.is_closed:
|
|
155
|
+
await self._client.aclose()
|
|
156
|
+
self._client = None
|
|
157
|
+
|
|
111
158
|
@staticmethod
|
|
112
159
|
def _validate_retry_attempts(retry_attempts: int) -> None:
|
|
113
160
|
if not isinstance(retry_attempts, int) or retry_attempts < 0:
|
|
@@ -123,6 +170,11 @@ class ConnectionPool:
|
|
|
123
170
|
if not isinstance(backoff_jitter, (int, float)) or backoff_jitter < 0:
|
|
124
171
|
raise ValueError("backoff_jitter must be a positive number")
|
|
125
172
|
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _validate_timeout(timeout: float) -> None:
|
|
175
|
+
if not isinstance(timeout, (int, float)) or timeout <= 0:
|
|
176
|
+
raise ValueError("timeout must be a positive number")
|
|
177
|
+
|
|
126
178
|
@property
|
|
127
179
|
def limits(self) -> Limits:
|
|
128
180
|
"""
|
|
@@ -141,7 +193,8 @@ class ConnectionPool:
|
|
|
141
193
|
f"keepalive_expiry={self._limits.keepalive_expiry}, "
|
|
142
194
|
f"retry_attempts={self._retry.total}, "
|
|
143
195
|
f"backoff_factor={self._retry.backoff_factor}, "
|
|
144
|
-
f"backoff_jitter={self._retry.backoff_jitter}
|
|
196
|
+
f"backoff_jitter={self._retry.backoff_jitter}, "
|
|
197
|
+
f"timeout={self._timeout})"
|
|
145
198
|
)
|
|
146
199
|
|
|
147
200
|
def __str__(self) -> str:
|
|
@@ -129,25 +129,37 @@ class SendgridAPI(BaseSendgridAPI):
|
|
|
129
129
|
) -> Response:
|
|
130
130
|
"""
|
|
131
131
|
Make a Twilio SendGrid v3 API request with the request body generated
|
|
132
|
-
by the Mail object
|
|
132
|
+
by the Mail object.
|
|
133
|
+
|
|
134
|
+
.. warning::
|
|
135
|
+
Calling this method repeatedly with per-call ``retry`` or
|
|
136
|
+
``backoff`` overrides will open and close a new connection
|
|
137
|
+
each time, which can degrade throughput significantly.
|
|
138
|
+
For regular sends, configure the desired retry and backoff
|
|
139
|
+
on the ``ConnectionPool`` once at initialization.
|
|
133
140
|
|
|
134
141
|
Args:
|
|
135
142
|
email: The Twilio SendGrid v3 API request body generated
|
|
136
|
-
by the Mail object or dict
|
|
143
|
+
by the Mail object or dict.
|
|
137
144
|
retry: Override the number of retry attempts for this
|
|
138
|
-
request.
|
|
145
|
+
request. Creates an ephemeral client.
|
|
146
|
+
Uses the pool default when not set.
|
|
139
147
|
backoff: Override the backoff factor for this request.
|
|
140
|
-
|
|
148
|
+
Creates an ephemeral client.
|
|
149
|
+
Uses the pool default when not set.
|
|
141
150
|
|
|
142
151
|
Returns:
|
|
143
|
-
The Twilio SendGrid v3 API response
|
|
152
|
+
The Twilio SendGrid v3 API response.
|
|
144
153
|
"""
|
|
145
154
|
self._check_session_closed()
|
|
146
155
|
json_message = email.get()
|
|
147
156
|
|
|
148
157
|
if retry is not None or backoff is not None:
|
|
149
|
-
|
|
158
|
+
session = self._build_client(retry, backoff)
|
|
159
|
+
try:
|
|
150
160
|
return await self._send(session, json_message)
|
|
161
|
+
finally:
|
|
162
|
+
await session.aclose()
|
|
151
163
|
|
|
152
164
|
return await self._send(self._session, json_message)
|
|
153
165
|
|
|
@@ -161,7 +173,7 @@ class SendgridAPI(BaseSendgridAPI):
|
|
|
161
173
|
retry: Optional[int] = None,
|
|
162
174
|
backoff: Optional[float] = None,
|
|
163
175
|
) -> AsyncClient:
|
|
164
|
-
return self._pool.
|
|
176
|
+
return self._pool._create_ephemeral_client(
|
|
165
177
|
self._headers, retry=retry, backoff=backoff
|
|
166
178
|
)
|
|
167
179
|
|
|
@@ -169,13 +181,23 @@ class SendgridAPI(BaseSendgridAPI):
|
|
|
169
181
|
"""
|
|
170
182
|
Check if the session is closed.
|
|
171
183
|
|
|
184
|
+
If the pool was explicitly shut down, raises
|
|
185
|
+
``SessionClosedException``. Otherwise, transparently
|
|
186
|
+
rebuilds the underlying client.
|
|
187
|
+
|
|
172
188
|
Raises:
|
|
173
|
-
SessionClosedException: If the
|
|
189
|
+
SessionClosedException: If the pool has been shut down.
|
|
174
190
|
"""
|
|
175
|
-
if self._session.is_closed:
|
|
191
|
+
if not self._session.is_closed:
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
if self._pool.is_shutdown:
|
|
176
195
|
logger.error("Session not initialized")
|
|
177
196
|
raise SessionClosedException("Session not initialized")
|
|
178
197
|
|
|
198
|
+
logger.debug("Session closed unexpectedly, rebuilding client")
|
|
199
|
+
self._session = self._pool._create_client(self._headers)
|
|
200
|
+
|
|
179
201
|
def __repr__(self) -> str:
|
|
180
202
|
return (
|
|
181
203
|
f"SendgridAPI("
|
|
File without changes
|
|
File without changes
|
|
File without changes
|