Skip to content

Commit 11a8ef5

Browse files
Release 0.1.7
1 parent dfcebc6 commit 11a8ef5

22 files changed

Lines changed: 185 additions & 56 deletions

.fern/metadata.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"cliVersion": "2.13.0",
33
"generatorName": "fernapi/fern-python-sdk",
4-
"generatorVersion": "4.37.1",
4+
"generatorVersion": "4.41.2",
55
"generatorConfig": {
66
"client_class_name": "AgentMail",
77
"pydantic_config": {

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
- name: Set up python
1010
uses: actions/setup-python@v4
1111
with:
12-
python-version: 3.8
12+
python-version: 3.9
1313
- name: Bootstrap poetry
1414
run: |
1515
curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1
@@ -25,15 +25,15 @@ jobs:
2525
- name: Set up python
2626
uses: actions/setup-python@v4
2727
with:
28-
python-version: 3.8
28+
python-version: 3.9
2929
- name: Bootstrap poetry
3030
run: |
3131
curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1
3232
- name: Install dependencies
3333
run: poetry install
3434

3535
- name: Test
36-
run: poetry run pytest -rP .
36+
run: poetry run pytest -rP -n auto .
3737

3838
publish:
3939
needs: [compile, test]
@@ -45,7 +45,7 @@ jobs:
4545
- name: Set up python
4646
uses: actions/setup-python@v4
4747
with:
48-
python-version: 3.8
48+
python-version: 3.9
4949
- name: Bootstrap poetry
5050
run: |
5151
curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1

poetry.lock

Lines changed: 35 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "agentmail"
33

44
[tool.poetry]
55
name = "agentmail"
6-
version = "0.1.6"
6+
version = "0.1.7"
77
description = ""
88
readme = "README.md"
99
authors = []
@@ -45,6 +45,7 @@ websockets = ">=12.0"
4545
mypy = "==1.13.0"
4646
pytest = "^7.4.0"
4747
pytest-asyncio = "^0.23.5"
48+
pytest-xdist = "^3.6.1"
4849
python-dateutil = "^2.9.0"
4950
types-python-dateutil = "^2.9.0.20240316"
5051
ruff = "==0.11.5"

src/agentmail/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@
9797
Bounce,
9898
Complaint,
9999
Delivery,
100+
DomainVerifiedEvent,
100101
EventId,
102+
EventType,
103+
EventTypes,
101104
MessageBouncedEvent,
102105
MessageComplainedEvent,
103106
MessageDeliveredEvent,
@@ -206,6 +209,7 @@
206209
"DomainId": ".domains",
207210
"DomainItem": ".domains",
208211
"DomainName": ".domains",
212+
"DomainVerifiedEvent": ".events",
209213
"Draft": ".drafts",
210214
"DraftAttachments": ".drafts",
211215
"DraftBcc": ".drafts",
@@ -227,6 +231,8 @@
227231
"ErrorName": ".types",
228232
"ErrorResponse": ".types",
229233
"EventId": ".events",
234+
"EventType": ".events",
235+
"EventTypes": ".events",
230236
"FeedbackEnabled": ".domains",
231237
"IsTakenError": ".errors",
232238
"Labels": ".types",
@@ -384,6 +390,7 @@ def __dir__():
384390
"DomainId",
385391
"DomainItem",
386392
"DomainName",
393+
"DomainVerifiedEvent",
387394
"Draft",
388395
"DraftAttachments",
389396
"DraftBcc",
@@ -405,6 +412,8 @@ def __dir__():
405412
"ErrorName",
406413
"ErrorResponse",
407414
"EventId",
415+
"EventType",
416+
"EventTypes",
408417
"FeedbackEnabled",
409418
"IsTakenError",
410419
"Labels",

src/agentmail/client.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from .pods.client import AsyncPodsClient, PodsClient
2020
from .threads.client import AsyncThreadsClient, ThreadsClient
2121
from .webhooks.client import AsyncWebhooksClient, WebhooksClient
22+
from .websockets.client import AsyncWebsocketsClient, WebsocketsClient
2223

2324

2425
class AgentMail:
@@ -94,6 +95,7 @@ def __init__(
9495
self._drafts: typing.Optional[DraftsClient] = None
9596
self._metrics: typing.Optional[MetricsClient] = None
9697
self._threads: typing.Optional[ThreadsClient] = None
98+
self._websockets: typing.Optional[WebsocketsClient] = None
9799

98100
@property
99101
def inboxes(self):
@@ -159,6 +161,14 @@ def threads(self):
159161
self._threads = ThreadsClient(client_wrapper=self._client_wrapper)
160162
return self._threads
161163

164+
@property
165+
def websockets(self):
166+
if self._websockets is None:
167+
from .websockets.client import WebsocketsClient # noqa: E402
168+
169+
self._websockets = WebsocketsClient(client_wrapper=self._client_wrapper)
170+
return self._websockets
171+
162172

163173
class AsyncAgentMail:
164174
"""
@@ -233,6 +243,7 @@ def __init__(
233243
self._drafts: typing.Optional[AsyncDraftsClient] = None
234244
self._metrics: typing.Optional[AsyncMetricsClient] = None
235245
self._threads: typing.Optional[AsyncThreadsClient] = None
246+
self._websockets: typing.Optional[AsyncWebsocketsClient] = None
236247

237248
@property
238249
def inboxes(self):
@@ -297,3 +308,11 @@ def threads(self):
297308

298309
self._threads = AsyncThreadsClient(client_wrapper=self._client_wrapper)
299310
return self._threads
311+
312+
@property
313+
def websockets(self):
314+
if self._websockets is None:
315+
from .websockets.client import AsyncWebsocketsClient # noqa: E402
316+
317+
self._websockets = AsyncWebsocketsClient(client_wrapper=self._client_wrapper)
318+
return self._websockets

src/agentmail/core/client_wrapper.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ def __init__(
2323

2424
def get_headers(self) -> typing.Dict[str, str]:
2525
headers: typing.Dict[str, str] = {
26-
"User-Agent": "agentmail/0.1.6",
26+
"User-Agent": "agentmail/0.1.7",
2727
"X-Fern-Language": "Python",
2828
"X-Fern-SDK-Name": "agentmail",
29-
"X-Fern-SDK-Version": "0.1.6",
29+
"X-Fern-SDK-Version": "0.1.7",
3030
**(self.get_custom_headers() or {}),
3131
}
3232
headers["Authorization"] = f"Bearer {self._get_api_key()}"

src/agentmail/core/http_client.py

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
from .request_options import RequestOptions
1919
from httpx._types import RequestFiles
2020

21-
INITIAL_RETRY_DELAY_SECONDS = 0.5
22-
MAX_RETRY_DELAY_SECONDS = 10
23-
MAX_RETRY_DELAY_SECONDS_FROM_HEADER = 30
21+
INITIAL_RETRY_DELAY_SECONDS = 1.0
22+
MAX_RETRY_DELAY_SECONDS = 60.0
23+
JITTER_FACTOR = 0.2 # 20% random jitter
2424

2525

2626
def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]:
@@ -64,24 +64,58 @@ def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float
6464
return seconds
6565

6666

67+
def _add_positive_jitter(delay: float) -> float:
68+
"""Add positive jitter (0-20%) to prevent thundering herd."""
69+
jitter_multiplier = 1 + random() * JITTER_FACTOR
70+
return delay * jitter_multiplier
71+
72+
73+
def _add_symmetric_jitter(delay: float) -> float:
74+
"""Add symmetric jitter (±10%) for exponential backoff."""
75+
jitter_multiplier = 1 + (random() - 0.5) * JITTER_FACTOR
76+
return delay * jitter_multiplier
77+
78+
79+
def _parse_x_ratelimit_reset(response_headers: httpx.Headers) -> typing.Optional[float]:
80+
"""
81+
Parse the X-RateLimit-Reset header (Unix timestamp in seconds).
82+
Returns seconds to wait, or None if header is missing/invalid.
83+
"""
84+
reset_time_str = response_headers.get("x-ratelimit-reset")
85+
if reset_time_str is None:
86+
return None
87+
88+
try:
89+
reset_time = int(reset_time_str)
90+
delay = reset_time - time.time()
91+
if delay > 0:
92+
return delay
93+
except (ValueError, TypeError):
94+
pass
95+
96+
return None
97+
98+
6799
def _retry_timeout(response: httpx.Response, retries: int) -> float:
68100
"""
69101
Determine the amount of time to wait before retrying a request.
70102
This function begins by trying to parse a retry-after header from the response, and then proceeds to use exponential backoff
71103
with a jitter to determine the number of seconds to wait.
72104
"""
73105

74-
# If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says.
106+
# 1. Check Retry-After header first
75107
retry_after = _parse_retry_after(response.headers)
76-
if retry_after is not None and retry_after <= MAX_RETRY_DELAY_SECONDS_FROM_HEADER:
77-
return retry_after
108+
if retry_after is not None and retry_after > 0:
109+
return min(retry_after, MAX_RETRY_DELAY_SECONDS)
78110

79-
# Apply exponential backoff, capped at MAX_RETRY_DELAY_SECONDS.
80-
retry_delay = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS)
111+
# 2. Check X-RateLimit-Reset header (with positive jitter)
112+
ratelimit_reset = _parse_x_ratelimit_reset(response.headers)
113+
if ratelimit_reset is not None:
114+
return _add_positive_jitter(min(ratelimit_reset, MAX_RETRY_DELAY_SECONDS))
81115

82-
# Add a randomness / jitter to the retry delay to avoid overwhelming the server with retries.
83-
timeout = retry_delay * (1 - 0.25 * random())
84-
return timeout if timeout >= 0 else 0
116+
# 3. Fall back to exponential backoff (with symmetric jitter)
117+
backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS)
118+
return _add_symmetric_jitter(backoff)
85119

86120

87121
def _should_retry(response: httpx.Response) -> bool:

src/agentmail/events/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
Bounce,
1111
Complaint,
1212
Delivery,
13+
DomainVerifiedEvent,
1314
EventId,
15+
EventType,
16+
EventTypes,
1417
MessageBouncedEvent,
1518
MessageComplainedEvent,
1619
MessageDeliveredEvent,
@@ -26,7 +29,10 @@
2629
"Bounce": ".types",
2730
"Complaint": ".types",
2831
"Delivery": ".types",
32+
"DomainVerifiedEvent": ".types",
2933
"EventId": ".types",
34+
"EventType": ".types",
35+
"EventTypes": ".types",
3036
"MessageBouncedEvent": ".types",
3137
"MessageComplainedEvent": ".types",
3238
"MessageDeliveredEvent": ".types",
@@ -65,7 +71,10 @@ def __dir__():
6571
"Bounce",
6672
"Complaint",
6773
"Delivery",
74+
"DomainVerifiedEvent",
6875
"EventId",
76+
"EventType",
77+
"EventTypes",
6978
"MessageBouncedEvent",
7079
"MessageComplainedEvent",
7180
"MessageDeliveredEvent",

src/agentmail/events/types/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
from .bounce import Bounce
1010
from .complaint import Complaint
1111
from .delivery import Delivery
12+
from .domain_verified_event import DomainVerifiedEvent
1213
from .event_id import EventId
14+
from .event_type import EventType
15+
from .event_types import EventTypes
1316
from .message_bounced_event import MessageBouncedEvent
1417
from .message_complained_event import MessageComplainedEvent
1518
from .message_delivered_event import MessageDeliveredEvent
@@ -24,7 +27,10 @@
2427
"Bounce": ".bounce",
2528
"Complaint": ".complaint",
2629
"Delivery": ".delivery",
30+
"DomainVerifiedEvent": ".domain_verified_event",
2731
"EventId": ".event_id",
32+
"EventType": ".event_type",
33+
"EventTypes": ".event_types",
2834
"MessageBouncedEvent": ".message_bounced_event",
2935
"MessageComplainedEvent": ".message_complained_event",
3036
"MessageDeliveredEvent": ".message_delivered_event",
@@ -63,7 +69,10 @@ def __dir__():
6369
"Bounce",
6470
"Complaint",
6571
"Delivery",
72+
"DomainVerifiedEvent",
6673
"EventId",
74+
"EventType",
75+
"EventTypes",
6776
"MessageBouncedEvent",
6877
"MessageComplainedEvent",
6978
"MessageDeliveredEvent",

0 commit comments

Comments
 (0)