oneshot-python 0.40.0__tar.gz → 0.40.1__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.
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/CHANGELOG.md +6 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/PKG-INFO +1 -1
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/README.md +6 -1
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/oneshot/client.py +130 -77
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/oneshot/lookup_manifest.py +2 -2
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/pyproject.toml +1 -1
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/conftest.py +2 -2
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_access_token.py +2 -4
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_email_payload.py +3 -8
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_hunts.py +21 -0
- oneshot_python-0.40.1/tests/test_person_quote_id.py +87 -0
- oneshot_python-0.40.1/tests/test_sync_without_loop.py +72 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/.gitignore +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/oneshot/__init__.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/oneshot/_errors.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/oneshot/_types.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/oneshot/lookup.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/oneshot/physical_mail.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/oneshot/x402.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/__init__.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_approvals.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_balance.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_browser.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_budgets.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_charge_amount.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_compute.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_compute_manifest.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_compute_outputs.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_declared_routes.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_declared_worker_reads.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_domains.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_domains_signed.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_emergency_error.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_inbound_voice.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_inbox_notifications.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_linkedin.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_lookup.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_max_cost_header.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_paid_call_proof.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_payment_rejection.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_person_quote_credits.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_phones_pending.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_physical_mail.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_poll_backoff.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_product_find.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_request_id.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_signed_job_and_analytics.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_tag_receipt_value.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_top_up_credits.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_update_build.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_wait_false.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/tests/test_x402.py +0 -0
- {oneshot_python-0.40.0 → oneshot_python-0.40.1}/uv.lock +0 -0
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.40.1
|
|
4
|
+
|
|
5
|
+
- Restore synchronous pause/resume calls when no event loop is current.
|
|
6
|
+
- Send person quote IDs through the request header, excluding them from tool payloads.
|
|
7
|
+
- Describe Changes queries for published dataset attributes.
|
|
8
|
+
|
|
3
9
|
## 0.40.0 — breaking release (#1336)
|
|
4
10
|
|
|
5
11
|
- Renamed `commerce_search()` / `acommerce_search()` → `product_find()` / `aproduct_find()`, without aliases.
|
|
@@ -34,6 +34,11 @@ balance = client.call_free_get("/v1/wallet/balance")
|
|
|
34
34
|
|
|
35
35
|
### Async
|
|
36
36
|
|
|
37
|
+
Blocking `OneShotClient` methods reuse the current open event loop, or run on a
|
|
38
|
+
temporary loop when none is available (including after `asyncio.run()`). Inside
|
|
39
|
+
a running event loop, use the async method instead; blocking calls raise an
|
|
40
|
+
error naming the async method to await.
|
|
41
|
+
|
|
37
42
|
```python
|
|
38
43
|
import asyncio
|
|
39
44
|
from oneshot import OneShotClient
|
|
@@ -346,7 +351,7 @@ MIT
|
|
|
346
351
|
|
|
347
352
|
## Optional phone lookup
|
|
348
353
|
|
|
349
|
-
Phone lookup is off by default. Source-returned numbers and published business or record numbers still appear. `include_phone=True` adds
|
|
354
|
+
Phone lookup is off by default. Source-returned numbers and published business or record numbers still appear. `include_phone=True` adds the per-person add-on listed on the pricing page × the requested search limit (default 100) for a search, or once for profile/dossier, for up to that many people with no guaranteed hit. Fallbacks and callbacks cost nothing extra. A completed lookup with no phone match is charged; a failed job is refunded.
|
|
350
355
|
|
|
351
356
|
`max_cost` and per-transaction budgets apply to the full quote before authorization, even when credits cover it. Wallet and access-token sessions both obtain a quote before execution and retry with `x-quote-id`.
|
|
352
357
|
|
|
@@ -13,7 +13,7 @@ import json
|
|
|
13
13
|
import re
|
|
14
14
|
import time
|
|
15
15
|
from decimal import Decimal
|
|
16
|
-
from typing import Any, Optional
|
|
16
|
+
from typing import Any, Coroutine, Optional, TypeVar
|
|
17
17
|
from urllib.parse import quote
|
|
18
18
|
|
|
19
19
|
import httpx
|
|
@@ -60,7 +60,7 @@ try:
|
|
|
60
60
|
|
|
61
61
|
SDK_VERSION = _pkg_version("oneshot-python")
|
|
62
62
|
except Exception: # pragma: no cover - editable/source runs without dist metadata
|
|
63
|
-
SDK_VERSION = "0.40.
|
|
63
|
+
SDK_VERSION = "0.40.1"
|
|
64
64
|
|
|
65
65
|
_BASE_URL = "https://win.oneshotagent.com"
|
|
66
66
|
_HUNT_BASE_URL = "https://api.soulhunt.ai"
|
|
@@ -77,6 +77,31 @@ _POLL_RETRY_BASE = 2.0 # seconds, transient-error retry backoff base
|
|
|
77
77
|
_MAX_POLL_RETRIES = 3
|
|
78
78
|
_BUILD_TRANSPORT_KWARGS = ("max_cost", "timeout_sec", "wait", "approval_id", "idempotency_key")
|
|
79
79
|
|
|
80
|
+
_T = TypeVar("_T")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _run_sync(coro: Coroutine[Any, Any, _T], async_method: str) -> _T:
|
|
84
|
+
"""Run a blocking call on the current open loop, or a temporary loop."""
|
|
85
|
+
try:
|
|
86
|
+
asyncio.get_running_loop()
|
|
87
|
+
except RuntimeError:
|
|
88
|
+
pass
|
|
89
|
+
else:
|
|
90
|
+
# The wrapper already created this coroutine; do not leave it unawaited.
|
|
91
|
+
coro.close()
|
|
92
|
+
raise RuntimeError(
|
|
93
|
+
f"Cannot make a blocking SDK call inside a running event loop; "
|
|
94
|
+
f"use await client.{async_method}(...) instead."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
loop = asyncio.get_event_loop()
|
|
99
|
+
except RuntimeError:
|
|
100
|
+
loop = None
|
|
101
|
+
if loop is None or loop.is_closed():
|
|
102
|
+
return asyncio.run(coro)
|
|
103
|
+
return loop.run_until_complete(coro)
|
|
104
|
+
|
|
80
105
|
|
|
81
106
|
def _build_email_payload(
|
|
82
107
|
to: Optional[str],
|
|
@@ -486,8 +511,9 @@ class OneShotClient:
|
|
|
486
511
|
debug: bool = False,
|
|
487
512
|
) -> "OneShotClient":
|
|
488
513
|
"""Blocking wrapper for :meth:`acreate`."""
|
|
489
|
-
return
|
|
490
|
-
cls.acreate(access_token=access_token, address=address, base_url=base_url, debug=debug)
|
|
514
|
+
return _run_sync(
|
|
515
|
+
cls.acreate(access_token=access_token, address=address, base_url=base_url, debug=debug),
|
|
516
|
+
"acreate",
|
|
491
517
|
)
|
|
492
518
|
|
|
493
519
|
def _assert_wallet_session(self, action: str) -> None:
|
|
@@ -501,7 +527,7 @@ class OneShotClient:
|
|
|
501
527
|
return self._unwrap_data(await self.acall_free_post("/v1/agents/me/access-tokens", {"name": name} if name else {}, scope="write"))
|
|
502
528
|
|
|
503
529
|
def create_access_token(self, name: Optional[str] = None) -> AccessTokenCreated:
|
|
504
|
-
return
|
|
530
|
+
return _run_sync(self.acreate_access_token(name), "acreate_access_token")
|
|
505
531
|
|
|
506
532
|
async def alist_access_tokens(self, *, active_only: bool = False) -> list[AccessTokenInfo]:
|
|
507
533
|
self._assert_wallet_session("list access tokens")
|
|
@@ -510,14 +536,14 @@ class OneShotClient:
|
|
|
510
536
|
return data.get("tokens", data) if isinstance(data, dict) else data
|
|
511
537
|
|
|
512
538
|
def list_access_tokens(self, *, active_only: bool = False) -> list[AccessTokenInfo]:
|
|
513
|
-
return
|
|
539
|
+
return _run_sync(self.alist_access_tokens(active_only=active_only), "alist_access_tokens")
|
|
514
540
|
|
|
515
541
|
async def arevoke_access_token(self, token_id: str) -> None:
|
|
516
542
|
self._assert_wallet_session("revoke access tokens")
|
|
517
543
|
await self.acall_free_delete(f"/v1/agents/me/access-tokens/{quote(token_id, safe='')}", scope="write")
|
|
518
544
|
|
|
519
545
|
def revoke_access_token(self, token_id: str) -> None:
|
|
520
|
-
|
|
546
|
+
_run_sync(self.arevoke_access_token(token_id), "arevoke_access_token")
|
|
521
547
|
|
|
522
548
|
async def aprovision_voice_number(self) -> dict[str, Any]:
|
|
523
549
|
"""Get this agent a voice number without placing a call.
|
|
@@ -529,14 +555,14 @@ class OneShotClient:
|
|
|
529
555
|
return self._unwrap_data(await self.acall_free_post("/v1/tools/voice/numbers", {}, scope="write"))
|
|
530
556
|
|
|
531
557
|
def provision_voice_number(self) -> dict[str, Any]:
|
|
532
|
-
return
|
|
558
|
+
return _run_sync(self.aprovision_voice_number(), "aprovision_voice_number")
|
|
533
559
|
|
|
534
560
|
async def avoice_numbers(self) -> dict[str, Any]:
|
|
535
561
|
"""The agent's phone numbers, from ``provision_voice_number`` or the first outbound voice call."""
|
|
536
562
|
return self._unwrap_data(await self.acall_free_get("/v1/tools/voice/numbers"))
|
|
537
563
|
|
|
538
564
|
def voice_numbers(self) -> dict[str, Any]:
|
|
539
|
-
return
|
|
565
|
+
return _run_sync(self.avoice_numbers(), "avoice_numbers")
|
|
540
566
|
|
|
541
567
|
async def aset_inbound_voice(self, phone_number_id: str, config: dict[str, Any]) -> dict[str, Any]:
|
|
542
568
|
"""Answer calls to one of the agent's numbers (replaces the whole config).
|
|
@@ -548,14 +574,14 @@ class OneShotClient:
|
|
|
548
574
|
return self._unwrap_data(await self.acall_free_put(path, config))
|
|
549
575
|
|
|
550
576
|
def set_inbound_voice(self, phone_number_id: str, config: dict[str, Any]) -> dict[str, Any]:
|
|
551
|
-
return
|
|
577
|
+
return _run_sync(self.aset_inbound_voice(phone_number_id, config), "aset_inbound_voice")
|
|
552
578
|
|
|
553
579
|
async def aget_inbound_voice(self, phone_number_id: str) -> dict[str, Any]:
|
|
554
580
|
path = f"/v1/tools/voice/numbers/{quote(phone_number_id, safe='')}/inbound"
|
|
555
581
|
return self._unwrap_data(await self.acall_free_get(path))
|
|
556
582
|
|
|
557
583
|
def get_inbound_voice(self, phone_number_id: str) -> dict[str, Any]:
|
|
558
|
-
return
|
|
584
|
+
return _run_sync(self.aget_inbound_voice(phone_number_id), "aget_inbound_voice")
|
|
559
585
|
|
|
560
586
|
async def adisable_inbound_voice(self, phone_number_id: str) -> dict[str, Any]:
|
|
561
587
|
"""Stop answering calls to a number; the settings are kept, disabled."""
|
|
@@ -563,7 +589,7 @@ class OneShotClient:
|
|
|
563
589
|
return self._unwrap_data(await self.acall_free_delete(path, scope="write"))
|
|
564
590
|
|
|
565
591
|
def disable_inbound_voice(self, phone_number_id: str) -> dict[str, Any]:
|
|
566
|
-
return
|
|
592
|
+
return _run_sync(self.adisable_inbound_voice(phone_number_id), "adisable_inbound_voice")
|
|
567
593
|
|
|
568
594
|
async def ainbound_calls(
|
|
569
595
|
self,
|
|
@@ -586,14 +612,14 @@ class OneShotClient:
|
|
|
586
612
|
return self._unwrap_data(await self.acall_free_get("/v1/tools/voice/inbound", params or None))
|
|
587
613
|
|
|
588
614
|
def inbound_calls(self, **kwargs: Any) -> dict[str, Any]:
|
|
589
|
-
return
|
|
615
|
+
return _run_sync(self.ainbound_calls(**kwargs), "ainbound_calls")
|
|
590
616
|
|
|
591
617
|
async def ainbound_call(self, call_id: str) -> dict[str, Any]:
|
|
592
618
|
"""One inbound call, with transcript."""
|
|
593
619
|
return self._unwrap_data(await self.acall_free_get(f"/v1/tools/voice/inbound/{quote(call_id, safe='')}"))
|
|
594
620
|
|
|
595
621
|
def inbound_call(self, call_id: str) -> dict[str, Any]:
|
|
596
|
-
return
|
|
622
|
+
return _run_sync(self.ainbound_call(call_id), "ainbound_call")
|
|
597
623
|
|
|
598
624
|
@staticmethod
|
|
599
625
|
def _with_approval(headers: dict[str, str], approval_id: Optional[str]) -> dict[str, str]:
|
|
@@ -772,7 +798,7 @@ class OneShotClient:
|
|
|
772
798
|
|
|
773
799
|
def get_budgets(self) -> AgentBudgetStatus:
|
|
774
800
|
"""Current spend budget and today's utilization. Blocking."""
|
|
775
|
-
return
|
|
801
|
+
return _run_sync(self.aget_budgets(), "aget_budgets")
|
|
776
802
|
|
|
777
803
|
async def aget_budgets(self) -> AgentBudgetStatus:
|
|
778
804
|
"""Current spend budget and today's utilization. Async."""
|
|
@@ -789,6 +815,7 @@ class OneShotClient:
|
|
|
789
815
|
wait: bool = True,
|
|
790
816
|
wait_for_phones: bool = False,
|
|
791
817
|
phone_timeout_sec: int = 360,
|
|
818
|
+
quote_id: Optional[str] = None,
|
|
792
819
|
idempotency_key: Optional[str] = None,
|
|
793
820
|
approval_id: Optional[str] = None,
|
|
794
821
|
) -> Any:
|
|
@@ -802,8 +829,11 @@ class OneShotClient:
|
|
|
802
829
|
until Apollo's async phone-reveal webhook delivers phone numbers
|
|
803
830
|
(or until ``phone_timeout_sec`` expires, default 6 min — Apollo says
|
|
804
831
|
"several minutes"). Default is False for backwards compat.
|
|
832
|
+
|
|
833
|
+
``quote_id`` rides on the ``x-quote-id`` header from the first attempt,
|
|
834
|
+
as in :meth:`acall_tool`; it is never part of the quoted body.
|
|
805
835
|
"""
|
|
806
|
-
return
|
|
836
|
+
return _run_sync(
|
|
807
837
|
self.acall_tool(
|
|
808
838
|
endpoint,
|
|
809
839
|
payload,
|
|
@@ -812,9 +842,11 @@ class OneShotClient:
|
|
|
812
842
|
wait=wait,
|
|
813
843
|
wait_for_phones=wait_for_phones,
|
|
814
844
|
phone_timeout_sec=phone_timeout_sec,
|
|
845
|
+
quote_id=quote_id,
|
|
815
846
|
idempotency_key=idempotency_key,
|
|
816
847
|
approval_id=approval_id,
|
|
817
|
-
)
|
|
848
|
+
),
|
|
849
|
+
"acall_tool",
|
|
818
850
|
)
|
|
819
851
|
|
|
820
852
|
def call_lookup(self, name: str, /, **params: Any) -> Any:
|
|
@@ -831,7 +863,7 @@ class OneShotClient:
|
|
|
831
863
|
method, pricing, path, payload = lookup_request(entry, params)
|
|
832
864
|
if pricing in PAID_PRICING:
|
|
833
865
|
return self.call_tool(path, payload)
|
|
834
|
-
return
|
|
866
|
+
return _run_sync(self._acall_free_lookup(method, path, payload), "acall_lookup")
|
|
835
867
|
|
|
836
868
|
async def acall_lookup(self, name: str, /, **params: Any) -> Any:
|
|
837
869
|
"""Async form of :meth:`call_lookup`, sent through :meth:`acall_tool`."""
|
|
@@ -864,13 +896,14 @@ class OneShotClient:
|
|
|
864
896
|
phone_timeout_sec: int = 360,
|
|
865
897
|
) -> Any:
|
|
866
898
|
"""Wait for a previously dispatched job (e.g. from ``wait=False``) and return its result."""
|
|
867
|
-
return
|
|
899
|
+
return _run_sync(
|
|
868
900
|
self.await_result(
|
|
869
901
|
request_id,
|
|
870
902
|
timeout_sec=timeout_sec,
|
|
871
903
|
wait_for_phones=wait_for_phones,
|
|
872
904
|
phone_timeout_sec=phone_timeout_sec,
|
|
873
|
-
)
|
|
905
|
+
),
|
|
906
|
+
"await_result",
|
|
874
907
|
)
|
|
875
908
|
|
|
876
909
|
async def await_result(
|
|
@@ -1179,8 +1212,9 @@ class OneShotClient:
|
|
|
1179
1212
|
params: Optional[dict[str, str]] = None,
|
|
1180
1213
|
) -> Any:
|
|
1181
1214
|
"""GET a free endpoint (blocking)."""
|
|
1182
|
-
return
|
|
1183
|
-
self.acall_free_get(endpoint, params)
|
|
1215
|
+
return _run_sync(
|
|
1216
|
+
self.acall_free_get(endpoint, params),
|
|
1217
|
+
"acall_free_get",
|
|
1184
1218
|
)
|
|
1185
1219
|
|
|
1186
1220
|
async def acall_free_get(
|
|
@@ -1204,8 +1238,9 @@ class OneShotClient:
|
|
|
1204
1238
|
scope: str = "read",
|
|
1205
1239
|
) -> Any:
|
|
1206
1240
|
"""POST to a free endpoint (blocking). Mutations pass scope="write"."""
|
|
1207
|
-
return
|
|
1208
|
-
self.acall_free_post(endpoint, payload, scope=scope)
|
|
1241
|
+
return _run_sync(
|
|
1242
|
+
self.acall_free_post(endpoint, payload, scope=scope),
|
|
1243
|
+
"acall_free_post",
|
|
1209
1244
|
)
|
|
1210
1245
|
|
|
1211
1246
|
async def acall_free_post(
|
|
@@ -1238,8 +1273,9 @@ class OneShotClient:
|
|
|
1238
1273
|
scope: str = "read",
|
|
1239
1274
|
) -> Any:
|
|
1240
1275
|
"""PATCH a free endpoint (blocking). Mutations pass scope="write"."""
|
|
1241
|
-
return
|
|
1242
|
-
self.acall_free_patch(endpoint, payload, scope=scope)
|
|
1276
|
+
return _run_sync(
|
|
1277
|
+
self.acall_free_patch(endpoint, payload, scope=scope),
|
|
1278
|
+
"acall_free_patch",
|
|
1243
1279
|
)
|
|
1244
1280
|
|
|
1245
1281
|
async def acall_free_patch(
|
|
@@ -1266,8 +1302,9 @@ class OneShotClient:
|
|
|
1266
1302
|
payload: Optional[dict[str, Any]] = None,
|
|
1267
1303
|
) -> Any:
|
|
1268
1304
|
"""PUT to a signed mutation endpoint (blocking)."""
|
|
1269
|
-
return
|
|
1270
|
-
self.acall_free_put(endpoint, payload)
|
|
1305
|
+
return _run_sync(
|
|
1306
|
+
self.acall_free_put(endpoint, payload),
|
|
1307
|
+
"acall_free_put",
|
|
1271
1308
|
)
|
|
1272
1309
|
|
|
1273
1310
|
async def acall_free_put(
|
|
@@ -1290,8 +1327,9 @@ class OneShotClient:
|
|
|
1290
1327
|
scope: str = "read",
|
|
1291
1328
|
) -> Any:
|
|
1292
1329
|
"""DELETE a free endpoint (blocking). Mutations pass scope="write"."""
|
|
1293
|
-
return
|
|
1294
|
-
self.acall_free_delete(endpoint, scope=scope)
|
|
1330
|
+
return _run_sync(
|
|
1331
|
+
self.acall_free_delete(endpoint, scope=scope),
|
|
1332
|
+
"acall_free_delete",
|
|
1295
1333
|
)
|
|
1296
1334
|
|
|
1297
1335
|
async def acall_free_delete(
|
|
@@ -1333,7 +1371,7 @@ class OneShotClient:
|
|
|
1333
1371
|
|
|
1334
1372
|
def create_approval(self, action_summary: str, **kwargs: Any) -> dict[str, Any]:
|
|
1335
1373
|
"""Ask a named human to decide something. Blocking."""
|
|
1336
|
-
return
|
|
1374
|
+
return _run_sync(self.acreate_approval(action_summary, **kwargs), "acreate_approval")
|
|
1337
1375
|
|
|
1338
1376
|
async def aget_approval(self, approval_id: str) -> dict[str, Any]:
|
|
1339
1377
|
"""One approval — poll it for the decision. Async."""
|
|
@@ -1341,7 +1379,7 @@ class OneShotClient:
|
|
|
1341
1379
|
|
|
1342
1380
|
def get_approval(self, approval_id: str) -> dict[str, Any]:
|
|
1343
1381
|
"""One approval — poll it for the decision. Blocking."""
|
|
1344
|
-
return
|
|
1382
|
+
return _run_sync(self.aget_approval(approval_id), "aget_approval")
|
|
1345
1383
|
|
|
1346
1384
|
async def alist_approvals(self, status: Optional[str] = None, limit: Optional[int] = None) -> list[dict[str, Any]]:
|
|
1347
1385
|
"""This agent's approvals, newest first. Async."""
|
|
@@ -1351,7 +1389,7 @@ class OneShotClient:
|
|
|
1351
1389
|
|
|
1352
1390
|
def list_approvals(self, status: Optional[str] = None, limit: Optional[int] = None) -> list[dict[str, Any]]:
|
|
1353
1391
|
"""This agent's approvals, newest first. Blocking."""
|
|
1354
|
-
return
|
|
1392
|
+
return _run_sync(self.alist_approvals(status, limit), "alist_approvals")
|
|
1355
1393
|
|
|
1356
1394
|
async def acancel_approval(self, approval_id: str) -> dict[str, Any]:
|
|
1357
1395
|
"""Withdraw a pending approval. Async."""
|
|
@@ -1359,7 +1397,7 @@ class OneShotClient:
|
|
|
1359
1397
|
|
|
1360
1398
|
def cancel_approval(self, approval_id: str) -> dict[str, Any]:
|
|
1361
1399
|
"""Withdraw a pending approval. Blocking."""
|
|
1362
|
-
return
|
|
1400
|
+
return _run_sync(self.acancel_approval(approval_id), "acancel_approval")
|
|
1363
1401
|
|
|
1364
1402
|
async def aget_action_policy(self) -> Optional[dict[str, Any]]:
|
|
1365
1403
|
"""The agent's action policy, or None. Async."""
|
|
@@ -1367,7 +1405,7 @@ class OneShotClient:
|
|
|
1367
1405
|
|
|
1368
1406
|
def get_action_policy(self) -> Optional[dict[str, Any]]:
|
|
1369
1407
|
"""The agent's action policy, or None. Blocking."""
|
|
1370
|
-
return
|
|
1408
|
+
return _run_sync(self.aget_action_policy(), "aget_action_policy")
|
|
1371
1409
|
|
|
1372
1410
|
async def aset_action_policy(self, policy: dict[str, Any]) -> dict[str, Any]:
|
|
1373
1411
|
"""Replace the action policy (rules, approver, timeout_seconds, enabled). Wallet sessions only. Async."""
|
|
@@ -1375,7 +1413,7 @@ class OneShotClient:
|
|
|
1375
1413
|
|
|
1376
1414
|
def set_action_policy(self, policy: dict[str, Any]) -> dict[str, Any]:
|
|
1377
1415
|
"""Replace the action policy. Wallet sessions only. Blocking."""
|
|
1378
|
-
return
|
|
1416
|
+
return _run_sync(self.aset_action_policy(policy), "aset_action_policy")
|
|
1379
1417
|
|
|
1380
1418
|
async def adelete_action_policy(self) -> None:
|
|
1381
1419
|
"""Remove the action policy. Wallet sessions only. Async."""
|
|
@@ -1383,7 +1421,7 @@ class OneShotClient:
|
|
|
1383
1421
|
|
|
1384
1422
|
def delete_action_policy(self) -> None:
|
|
1385
1423
|
"""Remove the action policy. Wallet sessions only. Blocking."""
|
|
1386
|
-
|
|
1424
|
+
_run_sync(self.adelete_action_policy(), "adelete_action_policy")
|
|
1387
1425
|
|
|
1388
1426
|
@staticmethod
|
|
1389
1427
|
def _unwrap_data(envelope: Any) -> Any:
|
|
@@ -1410,7 +1448,7 @@ class OneShotClient:
|
|
|
1410
1448
|
memo: Optional[str] = None,
|
|
1411
1449
|
) -> Any:
|
|
1412
1450
|
"""Create a compute goal. Blocking."""
|
|
1413
|
-
return
|
|
1451
|
+
return _run_sync(
|
|
1414
1452
|
self.acompute(
|
|
1415
1453
|
objective,
|
|
1416
1454
|
params=params,
|
|
@@ -1422,7 +1460,8 @@ class OneShotClient:
|
|
|
1422
1460
|
approvals=approvals,
|
|
1423
1461
|
max_cost=max_cost,
|
|
1424
1462
|
memo=memo,
|
|
1425
|
-
)
|
|
1463
|
+
),
|
|
1464
|
+
"acompute",
|
|
1426
1465
|
)
|
|
1427
1466
|
|
|
1428
1467
|
async def acompute(
|
|
@@ -1469,8 +1508,9 @@ class OneShotClient:
|
|
|
1469
1508
|
|
|
1470
1509
|
def get_compute_goal(self, goal_id: str) -> Any:
|
|
1471
1510
|
"""Get goal status. Blocking."""
|
|
1472
|
-
return
|
|
1473
|
-
self.aget_compute_goal(goal_id)
|
|
1511
|
+
return _run_sync(
|
|
1512
|
+
self.aget_compute_goal(goal_id),
|
|
1513
|
+
"aget_compute_goal",
|
|
1474
1514
|
)
|
|
1475
1515
|
|
|
1476
1516
|
async def aget_compute_goal(self, goal_id: str) -> Any:
|
|
@@ -1479,8 +1519,9 @@ class OneShotClient:
|
|
|
1479
1519
|
|
|
1480
1520
|
def get_compute_tasks(self, goal_id: str) -> Any:
|
|
1481
1521
|
"""List tasks under a goal. Blocking."""
|
|
1482
|
-
return
|
|
1483
|
-
self.aget_compute_tasks(goal_id)
|
|
1522
|
+
return _run_sync(
|
|
1523
|
+
self.aget_compute_tasks(goal_id),
|
|
1524
|
+
"aget_compute_tasks",
|
|
1484
1525
|
)
|
|
1485
1526
|
|
|
1486
1527
|
async def aget_compute_tasks(self, goal_id: str) -> Any:
|
|
@@ -1491,8 +1532,9 @@ class OneShotClient:
|
|
|
1491
1532
|
"""List what a goal produced: the files its sandbox tasks left and the
|
|
1492
1533
|
tool results too large for a task row. The goal's short answer is
|
|
1493
1534
|
``result`` on ``get_compute_goal``. Blocking."""
|
|
1494
|
-
return
|
|
1495
|
-
self.aget_compute_outputs(goal_id)
|
|
1535
|
+
return _run_sync(
|
|
1536
|
+
self.aget_compute_outputs(goal_id),
|
|
1537
|
+
"aget_compute_outputs",
|
|
1496
1538
|
)
|
|
1497
1539
|
|
|
1498
1540
|
async def aget_compute_outputs(self, goal_id: str) -> Any:
|
|
@@ -1503,8 +1545,9 @@ class OneShotClient:
|
|
|
1503
1545
|
"""Read one output of a goal by the ``key`` from ``get_compute_outputs``
|
|
1504
1546
|
(or a task's ``outputs``). ``content`` is None when the output was too
|
|
1505
1547
|
large to keep (``readable`` False). Blocking."""
|
|
1506
|
-
return
|
|
1507
|
-
self.aget_compute_output(goal_id, key)
|
|
1548
|
+
return _run_sync(
|
|
1549
|
+
self.aget_compute_output(goal_id, key),
|
|
1550
|
+
"aget_compute_output",
|
|
1508
1551
|
)
|
|
1509
1552
|
|
|
1510
1553
|
async def aget_compute_output(self, goal_id: str, key: str) -> Any:
|
|
@@ -1515,8 +1558,9 @@ class OneShotClient:
|
|
|
1515
1558
|
|
|
1516
1559
|
def get_compute_budget(self, goal_id: str) -> Any:
|
|
1517
1560
|
"""Get budget breakdown. Blocking."""
|
|
1518
|
-
return
|
|
1519
|
-
self.aget_compute_budget(goal_id)
|
|
1561
|
+
return _run_sync(
|
|
1562
|
+
self.aget_compute_budget(goal_id),
|
|
1563
|
+
"aget_compute_budget",
|
|
1520
1564
|
)
|
|
1521
1565
|
|
|
1522
1566
|
async def aget_compute_budget(self, goal_id: str) -> Any:
|
|
@@ -1530,8 +1574,9 @@ class OneShotClient:
|
|
|
1530
1574
|
refund is deferred: the result then has ``status == "pending_settlement"``
|
|
1531
1575
|
(no exception is raised).
|
|
1532
1576
|
"""
|
|
1533
|
-
return
|
|
1534
|
-
self.acancel_compute_goal(goal_id, reason)
|
|
1577
|
+
return _run_sync(
|
|
1578
|
+
self.acancel_compute_goal(goal_id, reason),
|
|
1579
|
+
"acancel_compute_goal",
|
|
1535
1580
|
)
|
|
1536
1581
|
|
|
1537
1582
|
async def acancel_compute_goal(
|
|
@@ -1558,10 +1603,11 @@ class OneShotClient:
|
|
|
1558
1603
|
approved: Optional[bool] = None,
|
|
1559
1604
|
) -> Any:
|
|
1560
1605
|
"""Respond to a human-in-the-loop approval task. Blocking."""
|
|
1561
|
-
return
|
|
1606
|
+
return _run_sync(
|
|
1562
1607
|
self.arespond_to_compute_task(
|
|
1563
1608
|
goal_id, task_id, response=response, approved=approved
|
|
1564
|
-
)
|
|
1609
|
+
),
|
|
1610
|
+
"arespond_to_compute_task",
|
|
1565
1611
|
)
|
|
1566
1612
|
|
|
1567
1613
|
async def arespond_to_compute_task(
|
|
@@ -1584,8 +1630,9 @@ class OneShotClient:
|
|
|
1584
1630
|
|
|
1585
1631
|
def pause_compute_goal(self, goal_id: str, reason: Optional[str] = None) -> Any:
|
|
1586
1632
|
"""Pause a recurring goal. Blocking."""
|
|
1587
|
-
return
|
|
1588
|
-
self.apause_compute_goal(goal_id, reason)
|
|
1633
|
+
return _run_sync(
|
|
1634
|
+
self.apause_compute_goal(goal_id, reason),
|
|
1635
|
+
"apause_compute_goal",
|
|
1589
1636
|
)
|
|
1590
1637
|
|
|
1591
1638
|
async def apause_compute_goal(
|
|
@@ -1599,8 +1646,9 @@ class OneShotClient:
|
|
|
1599
1646
|
|
|
1600
1647
|
def resume_compute_goal(self, goal_id: str) -> Any:
|
|
1601
1648
|
"""Resume a paused recurring goal. Blocking."""
|
|
1602
|
-
return
|
|
1603
|
-
self.aresume_compute_goal(goal_id)
|
|
1649
|
+
return _run_sync(
|
|
1650
|
+
self.aresume_compute_goal(goal_id),
|
|
1651
|
+
"aresume_compute_goal",
|
|
1604
1652
|
)
|
|
1605
1653
|
|
|
1606
1654
|
async def aresume_compute_goal(self, goal_id: str) -> Any:
|
|
@@ -1611,8 +1659,9 @@ class OneShotClient:
|
|
|
1611
1659
|
|
|
1612
1660
|
def fund_compute_goal(self, goal_id: str, amount: float) -> Any:
|
|
1613
1661
|
"""Top up budget on a recurring goal. Blocking."""
|
|
1614
|
-
return
|
|
1615
|
-
self.afund_compute_goal(goal_id, amount)
|
|
1662
|
+
return _run_sync(
|
|
1663
|
+
self.afund_compute_goal(goal_id, amount),
|
|
1664
|
+
"afund_compute_goal",
|
|
1616
1665
|
)
|
|
1617
1666
|
|
|
1618
1667
|
async def afund_compute_goal(self, goal_id: str, amount: float) -> Any:
|
|
@@ -1641,7 +1690,7 @@ class OneShotClient:
|
|
|
1641
1690
|
|
|
1642
1691
|
def top_up_credits(self, amount: float) -> Any:
|
|
1643
1692
|
"""Blocking wrapper for :meth:`atop_up_credits`."""
|
|
1644
|
-
return
|
|
1693
|
+
return _run_sync(self.atop_up_credits(amount), "atop_up_credits")
|
|
1645
1694
|
|
|
1646
1695
|
def browser(
|
|
1647
1696
|
self,
|
|
@@ -1657,13 +1706,14 @@ class OneShotClient:
|
|
|
1657
1706
|
timeout_sec: int = 300,
|
|
1658
1707
|
) -> Any:
|
|
1659
1708
|
"""Run a browser automation task. Blocking."""
|
|
1660
|
-
return
|
|
1709
|
+
return _run_sync(
|
|
1661
1710
|
self.abrowser(
|
|
1662
1711
|
task, output_schema=output_schema, start_url=start_url,
|
|
1663
1712
|
allowed_domains=allowed_domains, profile_id=profile_id,
|
|
1664
1713
|
secrets=secrets, max_steps=max_steps, max_cost=max_cost,
|
|
1665
1714
|
timeout_sec=timeout_sec,
|
|
1666
|
-
)
|
|
1715
|
+
),
|
|
1716
|
+
"abrowser",
|
|
1667
1717
|
)
|
|
1668
1718
|
|
|
1669
1719
|
async def abrowser(
|
|
@@ -1842,7 +1892,7 @@ class OneShotClient:
|
|
|
1842
1892
|
return await self.acall_free_get(f"/v1/tools/linkedin/accounts/{quote(account_id, safe='')}/invitations", _linkedin_query(params))
|
|
1843
1893
|
|
|
1844
1894
|
def _linkedin_invitation_call(self, endpoint: str, payload: dict, **kwargs: Any) -> Any:
|
|
1845
|
-
return
|
|
1895
|
+
return _run_sync(self._alinkedin_invitation_call(endpoint, payload, **kwargs), "_alinkedin_invitation_call")
|
|
1846
1896
|
|
|
1847
1897
|
async def _alinkedin_invitation_call(self, endpoint: str, payload: dict, **kwargs: Any) -> Any:
|
|
1848
1898
|
try:
|
|
@@ -1979,13 +2029,14 @@ class OneShotClient:
|
|
|
1979
2029
|
``mailbox_mode == 'mailbox'``, and read ``mailbox_provisioning_fee`` on
|
|
1980
2030
|
the quote for the exact amount before paying.
|
|
1981
2031
|
"""
|
|
1982
|
-
return
|
|
2032
|
+
return _run_sync(
|
|
1983
2033
|
self.aemail(
|
|
1984
2034
|
to, subject, body,
|
|
1985
2035
|
from_domain=from_domain, from_mailbox=from_mailbox, from_name=from_name,
|
|
1986
2036
|
reply_to_email_id=reply_to_email_id, mailbox_mode=mailbox_mode,
|
|
1987
2037
|
**kwargs,
|
|
1988
|
-
)
|
|
2038
|
+
),
|
|
2039
|
+
"aemail",
|
|
1989
2040
|
)
|
|
1990
2041
|
|
|
1991
2042
|
async def aemail(
|
|
@@ -2082,7 +2133,7 @@ class OneShotClient:
|
|
|
2082
2133
|
"""Keep SDK controls out of the canonical quoted input."""
|
|
2083
2134
|
return {key: kwargs.pop(key) for key in (
|
|
2084
2135
|
"max_cost", "timeout_sec", "wait", "wait_for_phones",
|
|
2085
|
-
"phone_timeout_sec", "idempotency_key", "approval_id",
|
|
2136
|
+
"phone_timeout_sec", "idempotency_key", "approval_id", "quote_id",
|
|
2086
2137
|
) if key in kwargs}
|
|
2087
2138
|
|
|
2088
2139
|
def person_find(self, *, include_phone: bool = False, **kwargs: Any) -> Any:
|
|
@@ -2361,7 +2412,7 @@ class OneShotClient:
|
|
|
2361
2412
|
|
|
2362
2413
|
def pause_domain(self, domain: str) -> DomainPoolStatusResult:
|
|
2363
2414
|
"""Take a domain out of rotation without releasing it. Blocking."""
|
|
2364
|
-
return
|
|
2415
|
+
return _run_sync(self.apause_domain(domain), "apause_domain")
|
|
2365
2416
|
|
|
2366
2417
|
async def apause_domain(self, domain: str) -> DomainPoolStatusResult:
|
|
2367
2418
|
"""Take a domain out of rotation without releasing it. Async."""
|
|
@@ -2369,7 +2420,7 @@ class OneShotClient:
|
|
|
2369
2420
|
|
|
2370
2421
|
def resume_domain(self, domain: str) -> DomainPoolStatusResult:
|
|
2371
2422
|
"""Put a paused domain back into rotation. Blocking."""
|
|
2372
|
-
return
|
|
2423
|
+
return _run_sync(self.aresume_domain(domain), "aresume_domain")
|
|
2373
2424
|
|
|
2374
2425
|
async def aresume_domain(self, domain: str) -> DomainPoolStatusResult:
|
|
2375
2426
|
"""Put a paused domain back into rotation. Async.
|
|
@@ -2410,10 +2461,11 @@ class OneShotClient:
|
|
|
2410
2461
|
``savings``, ``engagement``. Stored as ``pending`` until a judge service
|
|
2411
2462
|
confirms them against inbound signals.
|
|
2412
2463
|
"""
|
|
2413
|
-
return
|
|
2464
|
+
return _run_sync(
|
|
2414
2465
|
self.atag_receipt_value(
|
|
2415
2466
|
receipt_id, value_tag, request_id=request_id, goal_id=goal_id
|
|
2416
|
-
)
|
|
2467
|
+
),
|
|
2468
|
+
"atag_receipt_value",
|
|
2417
2469
|
)
|
|
2418
2470
|
|
|
2419
2471
|
async def atag_receipt_value(
|
|
@@ -2685,7 +2737,7 @@ class HuntsClient:
|
|
|
2685
2737
|
return self.client._unwrap_data(resp.json())
|
|
2686
2738
|
|
|
2687
2739
|
def list(self, status: str = "active", limit: int = 20, offset: int = 0) -> list[dict[str, Any]]:
|
|
2688
|
-
return
|
|
2740
|
+
return _run_sync(self.alist(status, limit, offset), "hunts.alist")
|
|
2689
2741
|
|
|
2690
2742
|
async def astatus(self, hunt_id: str) -> dict[str, Any]:
|
|
2691
2743
|
async with httpx.AsyncClient(timeout=30.0) as transport:
|
|
@@ -2697,7 +2749,7 @@ class HuntsClient:
|
|
|
2697
2749
|
return self.client._unwrap_data(resp.json())
|
|
2698
2750
|
|
|
2699
2751
|
def status(self, hunt_id: str) -> dict[str, Any]:
|
|
2700
|
-
return
|
|
2752
|
+
return _run_sync(self.astatus(hunt_id), "hunts.astatus")
|
|
2701
2753
|
|
|
2702
2754
|
async def apredict(
|
|
2703
2755
|
self, hunt_id: str, predicted_tool: str, predicted_action: str, predicted_motivation: str
|
|
@@ -2719,8 +2771,9 @@ class HuntsClient:
|
|
|
2719
2771
|
def predict(
|
|
2720
2772
|
self, hunt_id: str, predicted_tool: str, predicted_action: str, predicted_motivation: str
|
|
2721
2773
|
) -> dict[str, Any]:
|
|
2722
|
-
return
|
|
2723
|
-
self.apredict(hunt_id, predicted_tool, predicted_action, predicted_motivation)
|
|
2774
|
+
return _run_sync(
|
|
2775
|
+
self.apredict(hunt_id, predicted_tool, predicted_action, predicted_motivation),
|
|
2776
|
+
"hunts.apredict",
|
|
2724
2777
|
)
|
|
2725
2778
|
|
|
2726
2779
|
async def acapture(self, hunt_id: str) -> dict[str, Any]:
|
|
@@ -2734,7 +2787,7 @@ class HuntsClient:
|
|
|
2734
2787
|
return self.client._unwrap_data(resp.json())
|
|
2735
2788
|
|
|
2736
2789
|
def capture(self, hunt_id: str) -> dict[str, Any]:
|
|
2737
|
-
return
|
|
2790
|
+
return _run_sync(self.acapture(hunt_id), "hunts.acapture")
|
|
2738
2791
|
|
|
2739
2792
|
async def acollection(self) -> list[dict[str, Any]]:
|
|
2740
2793
|
async with httpx.AsyncClient(timeout=30.0) as transport:
|
|
@@ -2746,7 +2799,7 @@ class HuntsClient:
|
|
|
2746
2799
|
return self.client._unwrap_data(resp.json())
|
|
2747
2800
|
|
|
2748
2801
|
def collection(self) -> list[dict[str, Any]]:
|
|
2749
|
-
return
|
|
2802
|
+
return _run_sync(self.acollection(), "hunts.acollection")
|
|
2750
2803
|
|
|
2751
2804
|
async def aleaderboard(
|
|
2752
2805
|
self, period: str = "all", sort: str = "captures", limit: int = 25, offset: int = 0
|
|
@@ -2763,7 +2816,7 @@ class HuntsClient:
|
|
|
2763
2816
|
def leaderboard(
|
|
2764
2817
|
self, period: str = "all", sort: str = "captures", limit: int = 25, offset: int = 0
|
|
2765
2818
|
) -> list[dict[str, Any]]:
|
|
2766
|
-
return
|
|
2819
|
+
return _run_sync(self.aleaderboard(period, sort, limit, offset), "hunts.aleaderboard")
|
|
2767
2820
|
|
|
2768
2821
|
|
|
2769
2822
|
def _new_idempotency_key() -> str:
|
|
@@ -1368,13 +1368,13 @@ LOOKUP_MANIFEST: list[dict[str, Any]] = [
|
|
|
1368
1368
|
"method": "POST",
|
|
1369
1369
|
"pricing": "fixed",
|
|
1370
1370
|
"title": "Changes",
|
|
1371
|
-
"description": "What changed about one entity since a date, from the observations OneShot recorded: each change has the earlier value, the later value, when it was seen and the source. Give `attribute` (the name of a data tool that keeps history), `since`, and exactly one of `key` and `input`. For healthcare_exclusions, input selects a held monthly release and optional exact name/NPI/UPIN; returns row membership and full-row additions/removals, with coverage. A corrected row is removal plus addition, never person identity. A window with no changes is a completed, charged result. Paid per call.",
|
|
1371
|
+
"description": "What changed about one entity since a date, from the observations OneShot recorded: each change has the earlier value, the later value, when it was seen and the source. Give `attribute` (the name of a data tool that keeps history, or of a published dataset attribute such as company_operating_authority), `since`, and exactly one of `key` and `input`. For healthcare_exclusions, input selects a held monthly release and optional exact name/NPI/UPIN; returns row membership and full-row additions/removals, with coverage. A corrected row is removal plus addition, never person identity. A window with no changes is a completed, charged result. Paid per call.",
|
|
1372
1372
|
"input_schema": {
|
|
1373
1373
|
"type": "object",
|
|
1374
1374
|
"properties": {
|
|
1375
1375
|
"attribute": {
|
|
1376
1376
|
"type": "string",
|
|
1377
|
-
"description": "Name of a data tool that keeps history, e.g. vehicle_identification",
|
|
1377
|
+
"description": "Name of a data tool that keeps history, e.g. vehicle_identification, or of a published dataset attribute, e.g. company_operating_authority",
|
|
1378
1378
|
},
|
|
1379
1379
|
"key": {
|
|
1380
1380
|
"type": "string",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "oneshot-python"
|
|
3
|
-
version = "0.40.
|
|
3
|
+
version = "0.40.1"
|
|
4
4
|
description = "Core Python SDK for the OneShot API — HTTP client with x402 payment handling — 48 tools"
|
|
5
5
|
readme = {text = "Core Python SDK for the OneShot API", content-type = "text/plain"}
|
|
6
6
|
license = "MIT"
|
|
@@ -10,8 +10,8 @@ def blocking_test_event_loop(request):
|
|
|
10
10
|
if inspect.iscoroutinefunction(request.function):
|
|
11
11
|
yield
|
|
12
12
|
return
|
|
13
|
-
#
|
|
14
|
-
#
|
|
13
|
+
# Exercise reuse of a caller-owned loop, independent of async test cleanup.
|
|
14
|
+
# test_sync_without_loop covers the absent/closed-loop fallback separately.
|
|
15
15
|
loop = asyncio.new_event_loop()
|
|
16
16
|
asyncio.set_event_loop(loop)
|
|
17
17
|
try:
|
|
@@ -31,14 +31,12 @@ BASE = "http://test.local"
|
|
|
31
31
|
|
|
32
32
|
@pytest.fixture(autouse=True)
|
|
33
33
|
def _fresh_event_loop():
|
|
34
|
-
"""
|
|
35
|
-
closes loops between async tests, so give every test a fresh one."""
|
|
34
|
+
"""Exercise sync wrappers with a caller-owned loop for each test."""
|
|
36
35
|
loop = asyncio.new_event_loop()
|
|
37
36
|
asyncio.set_event_loop(loop)
|
|
38
37
|
yield
|
|
39
38
|
loop.close()
|
|
40
|
-
# Leave an
|
|
41
|
-
# sync wrappers call get_event_loop().run_until_complete on the current one.
|
|
39
|
+
# Leave an open loop for later tests that drive coroutines directly.
|
|
42
40
|
asyncio.set_event_loop(asyncio.new_event_loop())
|
|
43
41
|
|
|
44
42
|
|
|
@@ -25,14 +25,9 @@ REPLY_ID = "4fa85f64-5717-4562-b3fc-2c963f66afa6"
|
|
|
25
25
|
def _fresh_event_loop():
|
|
26
26
|
"""Give every test in this module its own event loop.
|
|
27
27
|
|
|
28
|
-
The sync surface (``email()``) bridges to ``aemail()`` via
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
breaks any *sync* test — mocked or not — that happens to run afterward
|
|
32
|
-
in the same session ("There is no current event loop in thread
|
|
33
|
-
'MainThread'"). Without this fixture the suite's pass/fail would depend
|
|
34
|
-
on test collection order, which is exactly the kind of environment-
|
|
35
|
-
dependent test this correction round was asked to avoid introducing.
|
|
28
|
+
The sync surface (``email()``) bridges to ``aemail()`` via ``_run_sync``.
|
|
29
|
+
Keep these payload tests on a caller-owned loop; absent and closed loops
|
|
30
|
+
are covered separately in ``test_sync_without_loop.py``.
|
|
36
31
|
"""
|
|
37
32
|
loop = asyncio.new_event_loop()
|
|
38
33
|
asyncio.set_event_loop(loop)
|
|
@@ -131,6 +131,27 @@ def test_default_and_overridden_hunt_url():
|
|
|
131
131
|
assert custom.hunt_url == "https://custom.hunt.example"
|
|
132
132
|
|
|
133
133
|
|
|
134
|
+
@pytest.mark.parametrize("sync_name,args", [
|
|
135
|
+
("list", ()),
|
|
136
|
+
("status", ("hunt_1",)),
|
|
137
|
+
("predict", ("hunt_1", "tool", "action", "motivation")),
|
|
138
|
+
("capture", ("hunt_1",)),
|
|
139
|
+
("collection", ()),
|
|
140
|
+
("leaderboard", ()),
|
|
141
|
+
])
|
|
142
|
+
def test_sync_inside_running_loop_names_bound_async_method(sync_name, args):
|
|
143
|
+
client = _wallet_client()
|
|
144
|
+
async_name = "a" + sync_name
|
|
145
|
+
assert callable(getattr(client.hunts, async_name))
|
|
146
|
+
|
|
147
|
+
async def scenario():
|
|
148
|
+
with pytest.raises(RuntimeError) as error:
|
|
149
|
+
getattr(client.hunts, sync_name)(*args)
|
|
150
|
+
assert f"use await client.hunts.{async_name}(...) instead." in str(error.value)
|
|
151
|
+
|
|
152
|
+
asyncio.run(scenario())
|
|
153
|
+
|
|
154
|
+
|
|
134
155
|
# ---------------------------------------------------------------- wallet session: reads
|
|
135
156
|
|
|
136
157
|
@pytest.mark.asyncio
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""A person call's ``quote_id`` is the ``x-quote-id`` header, never body input (#1945).
|
|
2
|
+
|
|
3
|
+
``person_find(quote_id=...)``, ``person_profile``, ``person_dossier`` and their
|
|
4
|
+
async variants hand ``quote_id`` to ``call_tool`` / ``acall_tool`` with the other
|
|
5
|
+
SDK controls, so the quoted input the API hashes is the person input alone and
|
|
6
|
+
the header carries the quote from the very first attempt.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from oneshot.client import OneShotClient
|
|
16
|
+
|
|
17
|
+
TEST_PRIVATE_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
|
|
18
|
+
QUOTE_ID = "q-person-1945"
|
|
19
|
+
PERSON_METHODS = [
|
|
20
|
+
("person_find", "/v1/tools/find/person"),
|
|
21
|
+
("person_profile", "/v1/tools/lookup/person-profile"),
|
|
22
|
+
("person_dossier", "/v1/tools/lookup/person-dossier"),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _queued() -> MagicMock:
|
|
27
|
+
resp = MagicMock()
|
|
28
|
+
resp.status_code = 202
|
|
29
|
+
resp.is_success = True
|
|
30
|
+
resp.json.return_value = {"request_id": "job-1", "status": "processing"}
|
|
31
|
+
resp.text = ""
|
|
32
|
+
resp.headers = {}
|
|
33
|
+
return resp
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _http() -> MagicMock:
|
|
37
|
+
http = MagicMock()
|
|
38
|
+
http.post = AsyncMock(return_value=_queued())
|
|
39
|
+
http.__aenter__ = AsyncMock(return_value=http)
|
|
40
|
+
http.__aexit__ = AsyncMock(return_value=False)
|
|
41
|
+
return http
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _client() -> OneShotClient:
|
|
45
|
+
client = OneShotClient(TEST_PRIVATE_KEY)
|
|
46
|
+
client._budget_synced = True
|
|
47
|
+
return client
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _assert_header_only(http: MagicMock, endpoint: str) -> None:
|
|
51
|
+
http.post.assert_awaited_once()
|
|
52
|
+
call = http.post.await_args
|
|
53
|
+
assert call.args[0].endswith(endpoint)
|
|
54
|
+
assert call.kwargs["headers"]["x-quote-id"] == QUOTE_ID
|
|
55
|
+
assert "quote_id" not in call.kwargs["json"]
|
|
56
|
+
assert call.kwargs["json"] == {"name": "Jane", "include_phone": True}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.mark.parametrize("method,endpoint", PERSON_METHODS)
|
|
60
|
+
def test_blocking_person_call_sends_quote_id_as_the_header_and_not_in_the_body(method, endpoint):
|
|
61
|
+
client = _client()
|
|
62
|
+
http = _http()
|
|
63
|
+
with patch("oneshot.client.httpx.AsyncClient", return_value=http):
|
|
64
|
+
result = getattr(client, method)(name="Jane", include_phone=True, quote_id=QUOTE_ID, wait=False)
|
|
65
|
+
assert result["request_id"] == "job-1"
|
|
66
|
+
_assert_header_only(http, endpoint)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.asyncio
|
|
70
|
+
@pytest.mark.parametrize("method,endpoint", PERSON_METHODS)
|
|
71
|
+
async def test_async_person_call_sends_quote_id_as_the_header_and_not_in_the_body(method, endpoint):
|
|
72
|
+
client = _client()
|
|
73
|
+
http = _http()
|
|
74
|
+
with patch("oneshot.client.httpx.AsyncClient", return_value=http):
|
|
75
|
+
result = await getattr(client, f"a{method}")(name="Jane", include_phone=True, quote_id=QUOTE_ID, wait=False)
|
|
76
|
+
assert result["request_id"] == "job-1"
|
|
77
|
+
_assert_header_only(http, endpoint)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_call_tool_forwards_quote_id_to_the_async_flow():
|
|
81
|
+
"""The blocking ``call_tool`` accepts ``quote_id`` and sends the same header."""
|
|
82
|
+
client = _client()
|
|
83
|
+
http = _http()
|
|
84
|
+
with patch("oneshot.client.httpx.AsyncClient", return_value=http):
|
|
85
|
+
client.call_tool("/v1/tools/find/person", {"name": "Jane"}, quote_id=QUOTE_ID, wait=False)
|
|
86
|
+
assert http.post.await_args.kwargs["headers"]["x-quote-id"] == QUOTE_ID
|
|
87
|
+
assert http.post.await_args.kwargs["json"] == {"name": "Jane"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Blocking SDK calls tolerate absent/closed loops and reject running loops."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from unittest.mock import AsyncMock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from oneshot.client import OneShotClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def client():
|
|
13
|
+
# Restore the caller's loop so these regressions do not alter other tests.
|
|
14
|
+
try:
|
|
15
|
+
previous_loop = asyncio.get_event_loop()
|
|
16
|
+
except RuntimeError:
|
|
17
|
+
previous_loop = None
|
|
18
|
+
try:
|
|
19
|
+
yield OneShotClient(access_token="oneshot_" + "a" * 64, address="0x" + "1" * 40)
|
|
20
|
+
finally:
|
|
21
|
+
asyncio.set_event_loop(previous_loop)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
METHODS = [
|
|
25
|
+
("pause_domain", "apause_domain", ("example.com",)),
|
|
26
|
+
("resume_domain", "aresume_domain", ("example.com",)),
|
|
27
|
+
("voice_numbers", "avoice_numbers", ()),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.parametrize("sync_name,async_name,args", METHODS)
|
|
32
|
+
def test_sync_after_asyncio_run(client, sync_name, async_name, args):
|
|
33
|
+
asyncio.run(asyncio.sleep(0))
|
|
34
|
+
expected = {"result": sync_name}
|
|
35
|
+
with patch.object(client, async_name, new_callable=AsyncMock, return_value=expected) as twin:
|
|
36
|
+
assert getattr(client, sync_name)(*args) is expected
|
|
37
|
+
twin.assert_awaited_once_with(*args)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_sync_reuses_open_current_loop(client):
|
|
41
|
+
loop = asyncio.new_event_loop()
|
|
42
|
+
asyncio.set_event_loop(loop)
|
|
43
|
+
try:
|
|
44
|
+
with patch.object(client, "apause_domain", new_callable=AsyncMock,
|
|
45
|
+
side_effect=lambda *_: asyncio.get_running_loop()) as twin:
|
|
46
|
+
assert client.pause_domain("example.com") is loop
|
|
47
|
+
twin.assert_awaited_once_with("example.com")
|
|
48
|
+
assert asyncio.get_event_loop() is loop
|
|
49
|
+
assert not loop.is_closed()
|
|
50
|
+
finally:
|
|
51
|
+
loop.close()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_sync_with_closed_current_loop(client):
|
|
55
|
+
loop = asyncio.new_event_loop()
|
|
56
|
+
asyncio.set_event_loop(loop)
|
|
57
|
+
loop.close()
|
|
58
|
+
expected = {"pool_status": "paused"}
|
|
59
|
+
with patch.object(client, "apause_domain", new_callable=AsyncMock, return_value=expected) as twin:
|
|
60
|
+
assert client.pause_domain("example.com") is expected
|
|
61
|
+
twin.assert_awaited_once_with("example.com")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.parametrize("sync_name,async_name,args", METHODS)
|
|
65
|
+
def test_sync_inside_running_loop_names_async_method(client, sync_name, async_name, args):
|
|
66
|
+
async def scenario():
|
|
67
|
+
with patch.object(client, async_name, new_callable=AsyncMock) as twin:
|
|
68
|
+
with pytest.raises(RuntimeError, match=rf"running event loop.*{async_name}"):
|
|
69
|
+
getattr(client, sync_name)(*args)
|
|
70
|
+
twin.assert_not_awaited()
|
|
71
|
+
|
|
72
|
+
asyncio.run(scenario())
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|