e2a 2.3.0__tar.gz → 2.5.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.
- {e2a-2.3.0 → e2a-2.5.0}/CHANGELOG.md +52 -0
- {e2a-2.3.0 → e2a-2.5.0}/PKG-INFO +1 -1
- {e2a-2.3.0 → e2a-2.5.0}/pyproject.toml +1 -1
- {e2a-2.3.0 → e2a-2.5.0}/src/e2a/v1/api.py +46 -0
- {e2a-2.3.0 → e2a-2.5.0}/src/e2a/v1/async_client.py +53 -3
- {e2a-2.3.0 → e2a-2.5.0}/src/e2a/v1/client.py +37 -3
- {e2a-2.3.0 → e2a-2.5.0}/src/e2a/v1/generated/__init__.py +39 -5
- {e2a-2.3.0 → e2a-2.5.0}/tests/test_idempotency.py +53 -0
- {e2a-2.3.0 → e2a-2.5.0}/tests/test_v1_api.py +2 -2
- {e2a-2.3.0 → e2a-2.5.0}/tests/test_v1_async_client.py +2 -2
- {e2a-2.3.0 → e2a-2.5.0}/tests/test_v1_client.py +2 -2
- {e2a-2.3.0 → e2a-2.5.0}/.gitignore +0 -0
- {e2a-2.3.0 → e2a-2.5.0}/README.md +0 -0
- {e2a-2.3.0 → e2a-2.5.0}/codegen-requirements.txt +0 -0
- {e2a-2.3.0 → e2a-2.5.0}/src/e2a/__init__.py +0 -0
- {e2a-2.3.0 → e2a-2.5.0}/src/e2a/v1/__init__.py +0 -0
- {e2a-2.3.0 → e2a-2.5.0}/src/e2a/v1/generated/_internal.py +0 -0
- {e2a-2.3.0 → e2a-2.5.0}/src/e2a/v1/generated/github_com_Mnexa_AI_e2a_internal_identity.py +0 -0
- {e2a-2.3.0 → e2a-2.5.0}/src/e2a/v1/generated/internal_agent.py +0 -0
- {e2a-2.3.0 → e2a-2.5.0}/src/e2a/v1/handler.py +0 -0
- {e2a-2.3.0 → e2a-2.5.0}/src/e2a/v1/websocket.py +0 -0
- {e2a-2.3.0 → e2a-2.5.0}/tests/__init__.py +0 -0
- {e2a-2.3.0 → e2a-2.5.0}/tests/test_contract.py +0 -0
- {e2a-2.3.0 → e2a-2.5.0}/tests/test_e2e.py +0 -0
- {e2a-2.3.0 → e2a-2.5.0}/tests/test_exports.py +0 -0
- {e2a-2.3.0 → e2a-2.5.0}/tests/test_generated_models.py +0 -0
- {e2a-2.3.0 → e2a-2.5.0}/tests/test_v1_handler.py +0 -0
- {e2a-2.3.0 → e2a-2.5.0}/tests/test_v1_websocket.py +0 -0
- {e2a-2.3.0 → e2a-2.5.0}/uv.lock +0 -0
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.5.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Generated types for the per-user resource-limits primitive that
|
|
7
|
+
shipped with #158: `LimitsInfo`, `LimitsCaps`, `LimitsUsage`. These
|
|
8
|
+
describe the response shape of `GET /api/v1/users/me/limits`, which
|
|
9
|
+
the hosted dashboard uses to render the upgrade affordance and the
|
|
10
|
+
"you've used X of Y" surface. The high-level `E2AClient` doesn't
|
|
11
|
+
yet expose a typed helper for this endpoint — it's surfaced as a
|
|
12
|
+
dashboard-only concern today, and SDK consumers querying their own
|
|
13
|
+
usage should call `/agents` / `/messages` directly. The types are
|
|
14
|
+
emitted so anyone consuming the raw OpenAPI generation has the
|
|
15
|
+
shapes available.
|
|
16
|
+
|
|
17
|
+
### Notes
|
|
18
|
+
- No runtime client behavior changed in this release. If you're not
|
|
19
|
+
using the limits primitive (self-host deployments without a paid
|
|
20
|
+
tier), 2.5.0 is functionally identical to 2.4.0.
|
|
21
|
+
|
|
22
|
+
## 2.4.0
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- `idempotency_key` parameter on `E2AClient.approve_message()` and its
|
|
26
|
+
async counterpart (and on the lower-level `E2AApi.approve_message()`).
|
|
27
|
+
Approve fires a real SES send, so without a stable key a retry after
|
|
28
|
+
a transient failure could double-send. When supplied it's threaded
|
|
29
|
+
through as the `Idempotency-Key` header; when omitted the SDK mints
|
|
30
|
+
a fresh UUIDv4 per call — that gives network-layer retry safety only.
|
|
31
|
+
Supply a stable key derived from the review event (typically the
|
|
32
|
+
pending `message_id`) to dedupe across an explicit retry loop.
|
|
33
|
+
- `sort`, `from_`, `subject_contains`, `conversation_id`, `since`,
|
|
34
|
+
`until` kwargs on `E2AApi.list_messages()` and the high-level
|
|
35
|
+
`E2AClient.get_messages()` (sync + async). `sort` defaults
|
|
36
|
+
server-side to newest-first; pass `"asc"` for FIFO polling. The
|
|
37
|
+
substring filters are case-insensitive and capped at 200 chars
|
|
38
|
+
server-side. `since` / `until` accept RFC3339 timestamps and
|
|
39
|
+
bracket `created_at`. Filter values are encoded into `next_token`,
|
|
40
|
+
so continuation requests must keep the same filter values.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- **Default sort flipped to newest-first** on `GET /messages`. Prior
|
|
44
|
+
releases silently returned oldest-first for `direction=inbound` (the
|
|
45
|
+
SDK default) and newest-first for `direction=all`. A polling agent
|
|
46
|
+
that relied on FIFO drain order should now pass `sort="asc"` to
|
|
47
|
+
preserve the old behavior.
|
|
48
|
+
- `agent_mode` is now a required field on `RegisterAgentRequest`. The
|
|
49
|
+
server previously silently defaulted to `"cloud"` and then 400'd
|
|
50
|
+
with a cryptic "webhook_url is required" message; it now explicitly
|
|
51
|
+
rejects requests missing `agent_mode` with a clear error. Pydantic
|
|
52
|
+
v2 will raise a validation error if you instantiate the request
|
|
53
|
+
without it. Set `agent_mode="local"` or `"cloud"` explicitly.
|
|
54
|
+
|
|
3
55
|
## 2.3.0
|
|
4
56
|
|
|
5
57
|
### Added
|
{e2a-2.3.0 → e2a-2.5.0}/PKG-INFO
RENAMED
|
@@ -191,8 +191,44 @@ class E2AApi:
|
|
|
191
191
|
status: str = "unread",
|
|
192
192
|
page_size: int = 50,
|
|
193
193
|
token: Optional[str] = None,
|
|
194
|
+
sort: Optional[str] = None,
|
|
195
|
+
from_: Optional[str] = None,
|
|
196
|
+
subject_contains: Optional[str] = None,
|
|
197
|
+
conversation_id: Optional[str] = None,
|
|
198
|
+
since: Optional[str] = None,
|
|
199
|
+
until: Optional[str] = None,
|
|
194
200
|
) -> ListMessagesResponse:
|
|
201
|
+
"""List messages for an agent.
|
|
202
|
+
|
|
203
|
+
``sort`` defaults server-side to ``"desc"`` (newest first).
|
|
204
|
+
Pass ``"asc"`` for FIFO polling — drain the inbox in arrival
|
|
205
|
+
order. The choice is encoded in ``next_token`` so subsequent
|
|
206
|
+
pages keep the same order; switching mid-pagination returns
|
|
207
|
+
400.
|
|
208
|
+
|
|
209
|
+
``from_``, ``subject_contains``: case-insensitive substring
|
|
210
|
+
match (Postgres ILIKE). Capped server-side at 200 chars.
|
|
211
|
+
|
|
212
|
+
``conversation_id``: exact match — narrow to one thread.
|
|
213
|
+
|
|
214
|
+
``since`` / ``until``: RFC3339 timestamps (``datetime.isoformat()``
|
|
215
|
+
produces a valid value as long as it ends in ``Z`` or has a
|
|
216
|
+
timezone offset). Bracket on ``created_at`` (``>= since`` and
|
|
217
|
+
``< until``).
|
|
218
|
+
"""
|
|
195
219
|
params: dict[str, str] = {"status": status, "page_size": str(page_size)}
|
|
220
|
+
if sort:
|
|
221
|
+
params["sort"] = sort
|
|
222
|
+
if from_:
|
|
223
|
+
params["from"] = from_
|
|
224
|
+
if subject_contains:
|
|
225
|
+
params["subject_contains"] = subject_contains
|
|
226
|
+
if conversation_id:
|
|
227
|
+
params["conversation_id"] = conversation_id
|
|
228
|
+
if since:
|
|
229
|
+
params["since"] = since
|
|
230
|
+
if until:
|
|
231
|
+
params["until"] = until
|
|
196
232
|
if token:
|
|
197
233
|
params["token"] = token
|
|
198
234
|
resp = self._client.get(
|
|
@@ -264,17 +300,27 @@ class E2AApi:
|
|
|
264
300
|
self,
|
|
265
301
|
message_id: str,
|
|
266
302
|
overrides: Optional[ApprovePendingMessageRequest] = None,
|
|
303
|
+
idempotency_key: Optional[str] = None,
|
|
267
304
|
) -> ApprovePendingMessageResponse:
|
|
268
305
|
"""Approve a held outbound message.
|
|
269
306
|
|
|
270
307
|
Pass ``overrides`` to approve with edits (any subset of
|
|
271
308
|
subject / body_text / body_html / to / cc / bcc / attachments).
|
|
272
309
|
Pass ``None`` (the default) to approve the draft as-is.
|
|
310
|
+
|
|
311
|
+
``idempotency_key`` is sent as the ``Idempotency-Key`` header.
|
|
312
|
+
Approve fires a real SES send, so supplying a stable key
|
|
313
|
+
derived from the review event makes retries safe (the server
|
|
314
|
+
replays the original response instead of double-sending).
|
|
315
|
+
When omitted the SDK mints a fresh UUIDv4 per call — that
|
|
316
|
+
gives network-layer retry safety only; the per-call default
|
|
317
|
+
does not survive an explicit retry loop.
|
|
273
318
|
"""
|
|
274
319
|
payload = overrides.model_dump(by_alias=True, exclude_none=True) if overrides else {}
|
|
275
320
|
resp = self._client.post(
|
|
276
321
|
f"/api/v1/messages/{quote(message_id, safe='')}/approve",
|
|
277
322
|
json=payload,
|
|
323
|
+
headers=_idempotency_header(idempotency_key),
|
|
278
324
|
)
|
|
279
325
|
_check_response(resp)
|
|
280
326
|
return ApprovePendingMessageResponse.model_validate(resp.json())
|
|
@@ -164,8 +164,28 @@ class AsyncE2AApi:
|
|
|
164
164
|
status: str = "unread",
|
|
165
165
|
page_size: int = 50,
|
|
166
166
|
token: Optional[str] = None,
|
|
167
|
+
sort: Optional[str] = None,
|
|
168
|
+
from_: Optional[str] = None,
|
|
169
|
+
subject_contains: Optional[str] = None,
|
|
170
|
+
conversation_id: Optional[str] = None,
|
|
171
|
+
since: Optional[str] = None,
|
|
172
|
+
until: Optional[str] = None,
|
|
167
173
|
) -> ListMessagesResponse:
|
|
174
|
+
"""Async variant of :meth:`E2AApi.list_messages`. See that
|
|
175
|
+
method for the full filter / sort docs."""
|
|
168
176
|
params: dict[str, str] = {"status": status, "page_size": str(page_size)}
|
|
177
|
+
if sort:
|
|
178
|
+
params["sort"] = sort
|
|
179
|
+
if from_:
|
|
180
|
+
params["from"] = from_
|
|
181
|
+
if subject_contains:
|
|
182
|
+
params["subject_contains"] = subject_contains
|
|
183
|
+
if conversation_id:
|
|
184
|
+
params["conversation_id"] = conversation_id
|
|
185
|
+
if since:
|
|
186
|
+
params["since"] = since
|
|
187
|
+
if until:
|
|
188
|
+
params["until"] = until
|
|
169
189
|
if token:
|
|
170
190
|
params["token"] = token
|
|
171
191
|
resp = await self._client.get(
|
|
@@ -231,11 +251,15 @@ class AsyncE2AApi:
|
|
|
231
251
|
self,
|
|
232
252
|
message_id: str,
|
|
233
253
|
overrides: Optional[ApprovePendingMessageRequest] = None,
|
|
254
|
+
idempotency_key: Optional[str] = None,
|
|
234
255
|
) -> ApprovePendingMessageResponse:
|
|
256
|
+
"""Async variant of :meth:`E2AApi.approve_message`. ``idempotency_key``
|
|
257
|
+
closes the SES double-send window — see that method for details."""
|
|
235
258
|
payload = overrides.model_dump(by_alias=True, exclude_none=True) if overrides else {}
|
|
236
259
|
resp = await self._client.post(
|
|
237
260
|
f"/api/v1/messages/{quote(message_id, safe='')}/approve",
|
|
238
261
|
json=payload,
|
|
262
|
+
headers=_idempotency_header(idempotency_key),
|
|
239
263
|
)
|
|
240
264
|
_check_response(resp)
|
|
241
265
|
return ApprovePendingMessageResponse.model_validate(resp.json())
|
|
@@ -389,10 +413,35 @@ class AsyncE2AClient:
|
|
|
389
413
|
page_size: int = 50,
|
|
390
414
|
token: Optional[str] = None,
|
|
391
415
|
agent_email: Optional[str] = None,
|
|
416
|
+
sort: Optional[str] = None,
|
|
417
|
+
from_: Optional[str] = None,
|
|
418
|
+
subject_contains: Optional[str] = None,
|
|
419
|
+
conversation_id: Optional[str] = None,
|
|
420
|
+
since: Optional[str] = None,
|
|
421
|
+
until: Optional[str] = None,
|
|
392
422
|
) -> MessageList:
|
|
393
|
-
"""Fetch message summaries with ergonomic field names.
|
|
423
|
+
"""Fetch message summaries with ergonomic field names.
|
|
424
|
+
|
|
425
|
+
``sort`` defaults server-side to ``"desc"`` (newest first). Pass
|
|
426
|
+
``"asc"`` to drain the inbox in arrival order — FIFO polling.
|
|
427
|
+
|
|
428
|
+
Search filters (``from_``, ``subject_contains``, ``conversation_id``,
|
|
429
|
+
``since``, ``until``) match the sync client — see
|
|
430
|
+
:meth:`E2AApi.list_messages` for the full reference.
|
|
431
|
+
"""
|
|
394
432
|
email = self._require_agent_email(agent_email)
|
|
395
|
-
resp = await self.api.list_messages(
|
|
433
|
+
resp = await self.api.list_messages(
|
|
434
|
+
email,
|
|
435
|
+
status=status,
|
|
436
|
+
page_size=page_size,
|
|
437
|
+
token=token,
|
|
438
|
+
sort=sort,
|
|
439
|
+
from_=from_,
|
|
440
|
+
subject_contains=subject_contains,
|
|
441
|
+
conversation_id=conversation_id,
|
|
442
|
+
since=since,
|
|
443
|
+
until=until,
|
|
444
|
+
)
|
|
396
445
|
messages = [
|
|
397
446
|
MessageSummary(
|
|
398
447
|
message_id=m.message_id or "",
|
|
@@ -564,6 +613,7 @@ class AsyncE2AClient:
|
|
|
564
613
|
to: Optional[list[str]] = None,
|
|
565
614
|
cc: Optional[list[str]] = None,
|
|
566
615
|
bcc: Optional[list[str]] = None,
|
|
616
|
+
idempotency_key: Optional[str] = None,
|
|
567
617
|
):
|
|
568
618
|
any_override = any(
|
|
569
619
|
v is not None for v in (subject, body_text, body_html, to, cc, bcc)
|
|
@@ -580,7 +630,7 @@ class AsyncE2AClient:
|
|
|
580
630
|
if any_override
|
|
581
631
|
else None
|
|
582
632
|
)
|
|
583
|
-
return await self.api.approve_message(message_id, overrides)
|
|
633
|
+
return await self.api.approve_message(message_id, overrides, idempotency_key=idempotency_key)
|
|
584
634
|
|
|
585
635
|
async def reject_message(self, message_id: str, reason: str = ""):
|
|
586
636
|
return await self.api.reject_message(message_id, reason)
|
|
@@ -185,10 +185,36 @@ class E2AClient:
|
|
|
185
185
|
page_size: int = 50,
|
|
186
186
|
token: Optional[str] = None,
|
|
187
187
|
agent_email: Optional[str] = None,
|
|
188
|
+
sort: Optional[str] = None,
|
|
189
|
+
from_: Optional[str] = None,
|
|
190
|
+
subject_contains: Optional[str] = None,
|
|
191
|
+
conversation_id: Optional[str] = None,
|
|
192
|
+
since: Optional[str] = None,
|
|
193
|
+
until: Optional[str] = None,
|
|
188
194
|
) -> MessageList:
|
|
189
|
-
"""Fetch message summaries with ergonomic field names.
|
|
195
|
+
"""Fetch message summaries with ergonomic field names.
|
|
196
|
+
|
|
197
|
+
``sort`` defaults server-side to ``"desc"`` (newest first). Pass
|
|
198
|
+
``"asc"`` to drain the inbox in arrival order — FIFO polling.
|
|
199
|
+
|
|
200
|
+
``from_`` / ``subject_contains`` are case-insensitive substring
|
|
201
|
+
filters (capped at 200 chars server-side). ``conversation_id``
|
|
202
|
+
exact-matches a thread. ``since`` / ``until`` are RFC3339
|
|
203
|
+
timestamps bounding ``created_at``.
|
|
204
|
+
"""
|
|
190
205
|
email = self._require_agent_email(agent_email)
|
|
191
|
-
resp = self.api.list_messages(
|
|
206
|
+
resp = self.api.list_messages(
|
|
207
|
+
email,
|
|
208
|
+
status=status,
|
|
209
|
+
page_size=page_size,
|
|
210
|
+
token=token,
|
|
211
|
+
sort=sort,
|
|
212
|
+
from_=from_,
|
|
213
|
+
subject_contains=subject_contains,
|
|
214
|
+
conversation_id=conversation_id,
|
|
215
|
+
since=since,
|
|
216
|
+
until=until,
|
|
217
|
+
)
|
|
192
218
|
messages = [
|
|
193
219
|
MessageSummary(
|
|
194
220
|
message_id=m.message_id or "",
|
|
@@ -369,11 +395,19 @@ class E2AClient:
|
|
|
369
395
|
to: Optional[list[str]] = None,
|
|
370
396
|
cc: Optional[list[str]] = None,
|
|
371
397
|
bcc: Optional[list[str]] = None,
|
|
398
|
+
idempotency_key: Optional[str] = None,
|
|
372
399
|
):
|
|
373
400
|
"""Approve a held outbound message.
|
|
374
401
|
|
|
375
402
|
Pass any subset of overrides to approve with edits; pass none
|
|
376
403
|
to approve as-is.
|
|
404
|
+
|
|
405
|
+
``idempotency_key`` makes retries safe across the SES double-
|
|
406
|
+
send window. Supply a stable key derived from the review event
|
|
407
|
+
(e.g. the dashboard click id or the pending ``message_id``) to
|
|
408
|
+
make retries dedupe. When omitted the SDK mints a fresh UUIDv4
|
|
409
|
+
per call — that gives network-layer retry safety only; the
|
|
410
|
+
per-call default does not survive an explicit retry loop.
|
|
377
411
|
"""
|
|
378
412
|
any_override = any(
|
|
379
413
|
v is not None for v in (subject, body_text, body_html, to, cc, bcc)
|
|
@@ -390,7 +424,7 @@ class E2AClient:
|
|
|
390
424
|
if any_override
|
|
391
425
|
else None
|
|
392
426
|
)
|
|
393
|
-
return self.api.approve_message(message_id, overrides)
|
|
427
|
+
return self.api.approve_message(message_id, overrides, idempotency_key=idempotency_key)
|
|
394
428
|
|
|
395
429
|
def reject_message(self, message_id: str, reason: str = ""):
|
|
396
430
|
"""Reject a held outbound message. The optional reason is
|
|
@@ -143,6 +143,26 @@ class Domain(BaseModel):
|
|
|
143
143
|
verified_at: str | None = None
|
|
144
144
|
|
|
145
145
|
|
|
146
|
+
class LimitsCaps(BaseModel):
|
|
147
|
+
model_config = ConfigDict(
|
|
148
|
+
populate_by_name=True,
|
|
149
|
+
)
|
|
150
|
+
max_agents: int | None = None
|
|
151
|
+
max_domains: int | None = None
|
|
152
|
+
max_messages_month: int | None = None
|
|
153
|
+
max_storage_bytes: int | None = None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class LimitsUsage(BaseModel):
|
|
157
|
+
model_config = ConfigDict(
|
|
158
|
+
populate_by_name=True,
|
|
159
|
+
)
|
|
160
|
+
agents: int | None = None
|
|
161
|
+
domains: int | None = None
|
|
162
|
+
messages_month: int | None = None
|
|
163
|
+
storage_bytes: int | None = None
|
|
164
|
+
|
|
165
|
+
|
|
146
166
|
class ListAgentsResponse(BaseModel):
|
|
147
167
|
model_config = ConfigDict(
|
|
148
168
|
populate_by_name=True,
|
|
@@ -267,11 +287,15 @@ class RegisterAgentRequest(BaseModel):
|
|
|
267
287
|
model_config = ConfigDict(
|
|
268
288
|
populate_by_name=True,
|
|
269
289
|
)
|
|
270
|
-
agent_mode:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
290
|
+
agent_mode: Literal['local', 'cloud'] = Field(
|
|
291
|
+
...,
|
|
292
|
+
description='AgentMode selects how inbound mail is delivered. Required; must be "local" or "cloud". See the type-level docs for the difference.',
|
|
293
|
+
examples=['local'],
|
|
294
|
+
)
|
|
295
|
+
email: str | None = Field(None, examples=['my-bot@yourdomain.com'])
|
|
296
|
+
name: str | None = Field(None, examples=['My Bot'])
|
|
297
|
+
slug: str | None = Field(None, examples=['my-bot'])
|
|
298
|
+
webhook_url: str | None = Field(None, examples=['https://example.com/e2a/webhook'])
|
|
275
299
|
|
|
276
300
|
|
|
277
301
|
class RegisterAgentResponse(BaseModel):
|
|
@@ -405,6 +429,16 @@ class ApprovePendingMessageRequest(BaseModel):
|
|
|
405
429
|
to: list[str] | None = None
|
|
406
430
|
|
|
407
431
|
|
|
432
|
+
class LimitsInfo(BaseModel):
|
|
433
|
+
model_config = ConfigDict(
|
|
434
|
+
populate_by_name=True,
|
|
435
|
+
)
|
|
436
|
+
limits: LimitsCaps | None = None
|
|
437
|
+
plan_code: str | None = None
|
|
438
|
+
upgrade_url: str | None = None
|
|
439
|
+
usage: LimitsUsage | None = None
|
|
440
|
+
|
|
441
|
+
|
|
408
442
|
class ListMessagesResponse(BaseModel):
|
|
409
443
|
model_config = ConfigDict(
|
|
410
444
|
populate_by_name=True,
|
|
@@ -128,3 +128,56 @@ def test_high_level_client_reply_threads_idempotency_key(httpx_mock):
|
|
|
128
128
|
|
|
129
129
|
req = httpx_mock.get_request()
|
|
130
130
|
assert req.headers["Idempotency-Key"] == "client-reply-key"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# approve_message is also side-effectful — fires a real SES send when
|
|
134
|
+
# the reviewer approves a held draft. Without an Idempotency-Key a
|
|
135
|
+
# transient retry after a successful approve could double-send. Cover
|
|
136
|
+
# the same contract: auto-generated key by default, caller-supplied
|
|
137
|
+
# key passes through verbatim, high-level client threads it through.
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_approve_message_auto_generates_idempotency_key(httpx_mock):
|
|
141
|
+
httpx_mock.add_response(
|
|
142
|
+
url=f"{BASE}/api/v1/messages/msg_p/approve",
|
|
143
|
+
method="POST",
|
|
144
|
+
json={"status": "sent", "message_id": "msg_p", "method": "smtp", "edited": False},
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
with E2AApi(api_key="e2a_test") as api:
|
|
148
|
+
api.approve_message("msg_p")
|
|
149
|
+
|
|
150
|
+
req = httpx_mock.get_request()
|
|
151
|
+
key = req.headers["Idempotency-Key"]
|
|
152
|
+
assert key, "Idempotency-Key header not set"
|
|
153
|
+
assert UUIDV4_RE.match(key), f"key {key!r} is not a UUIDv4 hex/canonical shape"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_approve_message_honors_caller_supplied_key(httpx_mock):
|
|
157
|
+
httpx_mock.add_response(
|
|
158
|
+
url=f"{BASE}/api/v1/messages/msg_p/approve",
|
|
159
|
+
method="POST",
|
|
160
|
+
json={"status": "sent", "message_id": "msg_p", "method": "smtp", "edited": False},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
with E2AApi(api_key="e2a_test") as api:
|
|
164
|
+
api.approve_message("msg_p", idempotency_key="approve-key-1")
|
|
165
|
+
|
|
166
|
+
req = httpx_mock.get_request()
|
|
167
|
+
assert req.headers["Idempotency-Key"] == "approve-key-1"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_high_level_client_approve_threads_idempotency_key(httpx_mock):
|
|
171
|
+
httpx_mock.add_response(
|
|
172
|
+
url=f"{BASE}/api/v1/messages/msg_p/approve",
|
|
173
|
+
method="POST",
|
|
174
|
+
json={"status": "sent", "message_id": "msg_p", "method": "smtp", "edited": False},
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
with E2AClient(
|
|
178
|
+
api_key="e2a_test", agent_email="bot@test.dev"
|
|
179
|
+
) as client:
|
|
180
|
+
client.approve_message("msg_p", idempotency_key="high-level-approve-key")
|
|
181
|
+
|
|
182
|
+
req = httpx_mock.get_request()
|
|
183
|
+
assert req.headers["Idempotency-Key"] == "high-level-approve-key"
|
|
@@ -158,13 +158,13 @@ def test_register_agent(httpx_mock):
|
|
|
158
158
|
)
|
|
159
159
|
|
|
160
160
|
with E2AApi(api_key="k") as api:
|
|
161
|
-
result = api.register_agent(RegisterAgentRequest(slug="new"))
|
|
161
|
+
result = api.register_agent(RegisterAgentRequest(slug="new", agent_mode="local"))
|
|
162
162
|
|
|
163
163
|
assert isinstance(result, RegisterAgentResponse)
|
|
164
164
|
assert result.email == "new@agents.e2a.dev"
|
|
165
165
|
|
|
166
166
|
body = json.loads(httpx_mock.get_request().content)
|
|
167
|
-
assert body == {"slug": "new"}
|
|
167
|
+
assert body == {"slug": "new", "agent_mode": "local"}
|
|
168
168
|
|
|
169
169
|
|
|
170
170
|
def test_get_agent(httpx_mock):
|
|
@@ -294,10 +294,10 @@ async def test_register_agent_slug(httpx_mock):
|
|
|
294
294
|
)
|
|
295
295
|
|
|
296
296
|
async with AsyncE2AClient(api_key="k") as client:
|
|
297
|
-
await client.register_agent("new")
|
|
297
|
+
await client.register_agent("new", agent_mode="local")
|
|
298
298
|
|
|
299
299
|
body = json.loads(httpx_mock.get_request().content)
|
|
300
|
-
assert body == {"slug": "new"}
|
|
300
|
+
assert body == {"slug": "new", "agent_mode": "local"}
|
|
301
301
|
|
|
302
302
|
|
|
303
303
|
@pytest.mark.anyio
|
|
@@ -442,10 +442,10 @@ def test_register_agent_slug_only(httpx_mock):
|
|
|
442
442
|
)
|
|
443
443
|
|
|
444
444
|
with E2AClient(api_key="k") as client:
|
|
445
|
-
result = client.register_agent("new")
|
|
445
|
+
result = client.register_agent("new", agent_mode="local")
|
|
446
446
|
|
|
447
447
|
body = json.loads(httpx_mock.get_request().content)
|
|
448
|
-
assert body == {"slug": "new"}
|
|
448
|
+
assert body == {"slug": "new", "agent_mode": "local"}
|
|
449
449
|
|
|
450
450
|
|
|
451
451
|
def test_register_agent_custom_domain(httpx_mock):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{e2a-2.3.0 → e2a-2.5.0}/uv.lock
RENAMED
|
File without changes
|