oneshot-python 0.39.0__tar.gz → 0.40.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.
- oneshot_python-0.40.0/CHANGELOG.md +7 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/PKG-INFO +1 -1
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/README.md +15 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/oneshot/client.py +62 -40
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/oneshot/lookup_manifest.py +73 -4
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/pyproject.toml +1 -1
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_person_quote_credits.py +48 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_phones_pending.py +6 -0
- oneshot_python-0.40.0/tests/test_product_find.py +18 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/.gitignore +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/oneshot/__init__.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/oneshot/_errors.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/oneshot/_types.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/oneshot/lookup.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/oneshot/physical_mail.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/oneshot/x402.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/__init__.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/conftest.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_access_token.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_approvals.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_balance.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_browser.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_budgets.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_charge_amount.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_compute.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_compute_manifest.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_compute_outputs.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_declared_routes.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_declared_worker_reads.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_domains.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_domains_signed.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_email_payload.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_emergency_error.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_hunts.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_inbound_voice.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_inbox_notifications.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_linkedin.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_lookup.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_max_cost_header.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_paid_call_proof.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_payment_rejection.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_physical_mail.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_poll_backoff.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_request_id.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_signed_job_and_analytics.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_tag_receipt_value.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_top_up_credits.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_update_build.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_wait_false.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/tests/test_x402.py +0 -0
- {oneshot_python-0.39.0 → oneshot_python-0.40.0}/uv.lock +0 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.40.0 — breaking release (#1336)
|
|
4
|
+
|
|
5
|
+
- Renamed `commerce_search()` / `acommerce_search()` → `product_find()` / `aproduct_find()`, without aliases.
|
|
6
|
+
- Product discovery now calls `POST /v1/tools/find/product` (price key `find/product`), replacing `POST /v1/tools/commerce/search` (`commerce/search`). Input, results and Purchase compatibility are unchanged.
|
|
7
|
+
- Release together with the person/company and other tool renames in #1334 and #1382; publish only with the coordinated server release.
|
|
@@ -343,3 +343,18 @@ async form).
|
|
|
343
343
|
## License
|
|
344
344
|
|
|
345
345
|
MIT
|
|
346
|
+
|
|
347
|
+
## Optional phone lookup
|
|
348
|
+
|
|
349
|
+
Phone lookup is off by default. Source-returned numbers and published business or record numbers still appear. `include_phone=True` adds $0.10 × the requested search limit (default 100 → $10), or $0.10 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
|
+
|
|
351
|
+
`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
|
+
|
|
353
|
+
```python
|
|
354
|
+
people = client.person_find(job_titles=["CTO"], limit=3, include_phone=True, max_cost=1, wait_for_phones=True)
|
|
355
|
+
profile = client.person_profile(email="jane@example.com", include_phone=True, max_cost=1, wait_for_phones=True)
|
|
356
|
+
dossier = client.person_dossier(email="jane@example.com", include_phone=True, max_cost=1, wait_for_phones=True, timeout_sec=600)
|
|
357
|
+
# Async equivalents: await client.aperson_find(...), aperson_profile(...), aperson_dossier(...).
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
`wait=False` returns the job and receipt IDs immediately. `wait_for_phones=True` keeps polling for pending phone callbacks after completion; it does not buy phones by itself. On timeout, the latest result can still have `phones_pending: True`.
|
|
@@ -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.
|
|
63
|
+
SDK_VERSION = "0.40.0"
|
|
64
64
|
|
|
65
65
|
_BASE_URL = "https://win.oneshotagent.com"
|
|
66
66
|
_HUNT_BASE_URL = "https://api.soulhunt.ai"
|
|
@@ -917,6 +917,7 @@ class OneShotClient:
|
|
|
917
917
|
on the ``x-quote-id`` header from the very first attempt, since
|
|
918
918
|
quote-based routes reject before the 402 dance even starts without it.
|
|
919
919
|
"""
|
|
920
|
+
person_quote = endpoint in ("/v1/tools/find/person", "/v1/tools/lookup/person-profile", "/v1/tools/lookup/person-dossier")
|
|
920
921
|
memo = payload.get("memo")
|
|
921
922
|
if memo is not None:
|
|
922
923
|
if not isinstance(memo, str) or not memo.strip():
|
|
@@ -999,7 +1000,7 @@ class OneShotClient:
|
|
|
999
1000
|
"processing",
|
|
1000
1001
|
):
|
|
1001
1002
|
if not wait:
|
|
1002
|
-
return result if endpoint.endswith(("/linkedin/invite", "/linkedin/invite/withdraw")) else {"request_id": result["request_id"], "status": result["status"]}
|
|
1003
|
+
return result if person_quote or endpoint.endswith(("/linkedin/invite", "/linkedin/invite/withdraw")) else {"request_id": result["request_id"], "status": result["status"]}
|
|
1003
1004
|
return await self._poll_job(
|
|
1004
1005
|
client,
|
|
1005
1006
|
result["request_id"],
|
|
@@ -1039,6 +1040,9 @@ class OneShotClient:
|
|
|
1039
1040
|
f"Quote ${total} exceeds max_cost ${max_cost}"
|
|
1040
1041
|
)
|
|
1041
1042
|
|
|
1043
|
+
if person_quote and total is not None:
|
|
1044
|
+
self._assert_within_budget(total)
|
|
1045
|
+
|
|
1042
1046
|
self._log(f"Payment required: {payment_request['amount']} USDC")
|
|
1043
1047
|
|
|
1044
1048
|
if self._access_token:
|
|
@@ -1068,7 +1072,7 @@ class OneShotClient:
|
|
|
1068
1072
|
result = resp2.json()
|
|
1069
1073
|
if isinstance(result, dict) and result.get("request_id") and result.get("status") in ("pending", "processing"):
|
|
1070
1074
|
if not wait:
|
|
1071
|
-
return result if endpoint.endswith(("/linkedin/invite", "/linkedin/invite/withdraw")) else {"request_id": result["request_id"], "status": result["status"]}
|
|
1075
|
+
return result if person_quote or endpoint.endswith(("/linkedin/invite", "/linkedin/invite/withdraw")) else {"request_id": result["request_id"], "status": result["status"]}
|
|
1072
1076
|
return await self._poll_job(
|
|
1073
1077
|
client, result["request_id"], timeout_sec,
|
|
1074
1078
|
wait_for_phones=wait_for_phones, phone_timeout_sec=phone_timeout_sec,
|
|
@@ -1158,7 +1162,7 @@ class OneShotClient:
|
|
|
1158
1162
|
):
|
|
1159
1163
|
self._log(f"Job queued: {result['request_id']}")
|
|
1160
1164
|
if not wait:
|
|
1161
|
-
return result if endpoint.endswith(("/linkedin/invite", "/linkedin/invite/withdraw")) else {"request_id": result["request_id"], "status": result["status"]}
|
|
1165
|
+
return result if person_quote or endpoint.endswith(("/linkedin/invite", "/linkedin/invite/withdraw")) else {"request_id": result["request_id"], "status": result["status"]}
|
|
1162
1166
|
return await self._poll_job(
|
|
1163
1167
|
client,
|
|
1164
1168
|
result["request_id"],
|
|
@@ -2073,21 +2077,33 @@ class OneShotClient:
|
|
|
2073
2077
|
approval_id = kwargs.pop("approval_id", None)
|
|
2074
2078
|
return await self.acall_tool("/v1/tools/sms/send", {"message": message, "to_number": to_number, **kwargs}, approval_id=approval_id)
|
|
2075
2079
|
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
def
|
|
2085
|
-
"""
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2080
|
+
@staticmethod
|
|
2081
|
+
def _person_call_options(kwargs: dict[str, Any]) -> dict[str, Any]:
|
|
2082
|
+
"""Keep SDK controls out of the canonical quoted input."""
|
|
2083
|
+
return {key: kwargs.pop(key) for key in (
|
|
2084
|
+
"max_cost", "timeout_sec", "wait", "wait_for_phones",
|
|
2085
|
+
"phone_timeout_sec", "idempotency_key", "approval_id",
|
|
2086
|
+
) if key in kwargs}
|
|
2087
|
+
|
|
2088
|
+
def person_find(self, *, include_phone: bool = False, **kwargs: Any) -> Any:
|
|
2089
|
+
"""Phone lookup is off by default; opt-in uses the limit (default 100) and the per-person add-on on the pricing page, with no hit guaranteed."""
|
|
2090
|
+
options = self._person_call_options(kwargs)
|
|
2091
|
+
return self.call_tool("/v1/tools/find/person", {**kwargs, "include_phone": include_phone}, **options)
|
|
2092
|
+
|
|
2093
|
+
async def aperson_find(self, *, include_phone: bool = False, **kwargs: Any) -> Any:
|
|
2094
|
+
"""Phone lookup is off by default; opt-in uses the limit (default 100) and the per-person add-on on the pricing page, with no hit guaranteed."""
|
|
2095
|
+
options = self._person_call_options(kwargs)
|
|
2096
|
+
return await self.acall_tool("/v1/tools/find/person", {**kwargs, "include_phone": include_phone}, **options)
|
|
2097
|
+
|
|
2098
|
+
def person_profile(self, *, include_phone: bool = False, **kwargs: Any) -> Any:
|
|
2099
|
+
"""Phone lookup is off by default; opt-in uses the add-on on the pricing page for one person, with no hit guaranteed."""
|
|
2100
|
+
options = self._person_call_options(kwargs)
|
|
2101
|
+
return self.call_tool("/v1/tools/lookup/person-profile", {**kwargs, "include_phone": include_phone}, **options)
|
|
2102
|
+
|
|
2103
|
+
async def aperson_profile(self, *, include_phone: bool = False, **kwargs: Any) -> Any:
|
|
2104
|
+
"""Phone lookup is off by default; opt-in uses the add-on on the pricing page for one person, with no hit guaranteed."""
|
|
2105
|
+
options = self._person_call_options(kwargs)
|
|
2106
|
+
return await self.acall_tool("/v1/tools/lookup/person-profile", {**kwargs, "include_phone": include_phone}, **options)
|
|
2091
2107
|
|
|
2092
2108
|
def company_find(self, **kwargs: Any) -> Any:
|
|
2093
2109
|
"""Search for companies by name, domain, industry, location, size, etc. Blocking."""
|
|
@@ -2159,13 +2175,15 @@ class OneShotClient:
|
|
|
2159
2175
|
"""Verify email deliverability. Async."""
|
|
2160
2176
|
return await self.acall_tool("/v1/tools/lookup/person-email-status", {"email": email})
|
|
2161
2177
|
|
|
2162
|
-
def person_dossier(self, **kwargs: Any) -> Any:
|
|
2163
|
-
"""
|
|
2164
|
-
|
|
2178
|
+
def person_dossier(self, *, include_phone: bool = False, **kwargs: Any) -> Any:
|
|
2179
|
+
"""Phone lookup is off by default; opt-in uses the add-on on the pricing page for one person, with no hit guaranteed."""
|
|
2180
|
+
options = self._person_call_options(kwargs)
|
|
2181
|
+
return self.call_tool("/v1/tools/lookup/person-dossier", {**kwargs, "include_phone": include_phone}, **options)
|
|
2165
2182
|
|
|
2166
|
-
async def aperson_dossier(self, **kwargs: Any) -> Any:
|
|
2167
|
-
"""
|
|
2168
|
-
|
|
2183
|
+
async def aperson_dossier(self, *, include_phone: bool = False, **kwargs: Any) -> Any:
|
|
2184
|
+
"""Phone lookup is off by default; opt-in uses the add-on on the pricing page for one person, with no hit guaranteed."""
|
|
2185
|
+
options = self._person_call_options(kwargs)
|
|
2186
|
+
return await self.acall_tool("/v1/tools/lookup/person-dossier", {**kwargs, "include_phone": include_phone}, **options)
|
|
2169
2187
|
|
|
2170
2188
|
def person_social_accounts(self, **kwargs: Any) -> Any:
|
|
2171
2189
|
"""Discover social media profiles. Blocking."""
|
|
@@ -2207,13 +2225,13 @@ class OneShotClient:
|
|
|
2207
2225
|
"""Get a person's social interactions. Async."""
|
|
2208
2226
|
return await self.acall_tool("/v1/tools/lookup/person-followers", {"social_media_url": social_media_url, **kwargs})
|
|
2209
2227
|
|
|
2210
|
-
def
|
|
2228
|
+
def product_find(self, query: str, *, limit: int = 10, **kwargs: Any) -> Any:
|
|
2211
2229
|
"""Search for products. Blocking."""
|
|
2212
|
-
return self.call_tool("/v1/tools/
|
|
2230
|
+
return self.call_tool("/v1/tools/find/product", {"query": query, "limit": limit, **kwargs})
|
|
2213
2231
|
|
|
2214
|
-
async def
|
|
2232
|
+
async def aproduct_find(self, query: str, *, limit: int = 10, **kwargs: Any) -> Any:
|
|
2215
2233
|
"""Search for products. Async."""
|
|
2216
|
-
return await self.acall_tool("/v1/tools/
|
|
2234
|
+
return await self.acall_tool("/v1/tools/find/product", {"query": query, "limit": limit, **kwargs})
|
|
2217
2235
|
|
|
2218
2236
|
def commerce_buy(self, product_url: str, shipping_address: dict[str, Any], **kwargs: Any) -> Any:
|
|
2219
2237
|
"""Purchase a product. Blocking."""
|
|
@@ -2515,23 +2533,27 @@ class OneShotClient:
|
|
|
2515
2533
|
@staticmethod
|
|
2516
2534
|
def _is_phones_pending(result: Any) -> bool:
|
|
2517
2535
|
"""Detects whether a result is awaiting Apollo's async phone-reveal
|
|
2518
|
-
webhook. Accepts
|
|
2519
|
-
|
|
2536
|
+
webhook. Accepts worker search rows, profile objects, dossier sections
|
|
2537
|
+
and legacy flat results.
|
|
2520
2538
|
|
|
2521
2539
|
Mirrors the TS SDK's _isPhonesPending — kept in lockstep so the worker
|
|
2522
2540
|
contract works for both SDKs.
|
|
2523
2541
|
"""
|
|
2524
2542
|
if not isinstance(result, dict):
|
|
2525
2543
|
return False
|
|
2526
|
-
|
|
2527
|
-
return True
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2544
|
+
def pending(value: Any) -> bool:
|
|
2545
|
+
return isinstance(value, dict) and value.get("phones_pending") is True
|
|
2546
|
+
|
|
2547
|
+
for body in (result, result.get("result")):
|
|
2548
|
+
if not isinstance(body, dict):
|
|
2549
|
+
continue
|
|
2550
|
+
profile = body.get("profile")
|
|
2551
|
+
data = profile.get("data") if isinstance(profile, dict) else None
|
|
2552
|
+
rows = body.get("results")
|
|
2553
|
+
if pending(body) or pending(profile) or pending(data):
|
|
2554
|
+
return True
|
|
2555
|
+
if isinstance(rows, list) and any(pending(row) for row in rows):
|
|
2556
|
+
return True
|
|
2535
2557
|
return False
|
|
2536
2558
|
|
|
2537
2559
|
async def _poll_for_phones(
|
|
@@ -563,6 +563,76 @@ LOOKUP_MANIFEST: list[dict[str, Any]] = [
|
|
|
563
563
|
"longitude": -95.3698,
|
|
564
564
|
},
|
|
565
565
|
},
|
|
566
|
+
{
|
|
567
|
+
"name": "market_freight_rate",
|
|
568
|
+
"path": "/v1/tools/lookup/market-freight-rate",
|
|
569
|
+
"title": "Freight Rate Quote",
|
|
570
|
+
"description": "Quote the rate to move freight between two US ZIP codes on a pickup date, by mode: ltl shares a trailer by the pallet, ftl is a whole 53-foot dry van, van is a cargo van and box_truck is a 26-foot box truck. The answer is one carrier's quote for that lane: the rate for the whole shipment with every included service, the transit time, the pickup and delivery dates, when the quote lapses and whether it is firm or indicative. Use it to check a billed freight charge against the lane's going rate. For ltl give the pallets, the weight per pallet and the pallet dimensions; for van and box_truck give the pallets and the weight per pallet; for ftl the lane and date are enough. A lane the mode does not serve is refused as invalid input: try another mode. Covers US lanes. Nothing is booked.",
|
|
571
|
+
"input_schema": {
|
|
572
|
+
"type": "object",
|
|
573
|
+
"properties": {
|
|
574
|
+
"origin_zip": {
|
|
575
|
+
"type": "string",
|
|
576
|
+
"description": "Where the freight is collected. 5-digit US ZIP code.",
|
|
577
|
+
},
|
|
578
|
+
"destination_zip": {
|
|
579
|
+
"type": "string",
|
|
580
|
+
"description": "Where the freight is delivered. 5-digit US ZIP code.",
|
|
581
|
+
},
|
|
582
|
+
"mode": {
|
|
583
|
+
"type": "string",
|
|
584
|
+
"enum": [
|
|
585
|
+
"ltl",
|
|
586
|
+
"ftl",
|
|
587
|
+
"van",
|
|
588
|
+
"box_truck",
|
|
589
|
+
],
|
|
590
|
+
"description": "How the freight moves. Picks the quote asked for; there is no default",
|
|
591
|
+
},
|
|
592
|
+
"pickup_date": {
|
|
593
|
+
"type": "string",
|
|
594
|
+
"description": "The day the freight is collected: today (UTC) or later. YYYY-MM-DD.",
|
|
595
|
+
},
|
|
596
|
+
"pallets": {
|
|
597
|
+
"type": "integer",
|
|
598
|
+
"description": "How many pallets. Required for ltl, van (up to 3) and box_truck (up to 12); not needed for ftl. At least 1.",
|
|
599
|
+
},
|
|
600
|
+
"weight_lbs_per_pallet": {
|
|
601
|
+
"type": "integer",
|
|
602
|
+
"description": "The weight of each pallet in pounds. Required for ltl, van (3,500 lb in all) and box_truck (10,000 lb in all); not needed for ftl. At least 1.",
|
|
603
|
+
},
|
|
604
|
+
"length_in": {
|
|
605
|
+
"type": "integer",
|
|
606
|
+
"description": "The length of each pallet in inches. Required for ltl; ignored for other modes. At least 1.",
|
|
607
|
+
},
|
|
608
|
+
"width_in": {
|
|
609
|
+
"type": "integer",
|
|
610
|
+
"description": "The width of each pallet in inches. Required for ltl; ignored for other modes. At least 1.",
|
|
611
|
+
},
|
|
612
|
+
"height_in": {
|
|
613
|
+
"type": "integer",
|
|
614
|
+
"description": "The height of each pallet in inches, up to 85. Required for ltl; ignored for other modes. At least 1.",
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
"required": [
|
|
618
|
+
"origin_zip",
|
|
619
|
+
"destination_zip",
|
|
620
|
+
"mode",
|
|
621
|
+
"pickup_date",
|
|
622
|
+
],
|
|
623
|
+
},
|
|
624
|
+
"example": {
|
|
625
|
+
"origin_zip": "90012",
|
|
626
|
+
"destination_zip": "94105",
|
|
627
|
+
"mode": "ltl",
|
|
628
|
+
"pickup_date": "2026-10-20",
|
|
629
|
+
"pallets": 2,
|
|
630
|
+
"weight_lbs_per_pallet": 800,
|
|
631
|
+
"length_in": 48,
|
|
632
|
+
"width_in": 40,
|
|
633
|
+
"height_in": 48,
|
|
634
|
+
},
|
|
635
|
+
},
|
|
566
636
|
{
|
|
567
637
|
"name": "person_health_provider",
|
|
568
638
|
"path": "/v1/tools/lookup/person-health-provider",
|
|
@@ -1298,7 +1368,7 @@ LOOKUP_MANIFEST: list[dict[str, Any]] = [
|
|
|
1298
1368
|
"method": "POST",
|
|
1299
1369
|
"pricing": "fixed",
|
|
1300
1370
|
"title": "Changes",
|
|
1301
|
-
"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`. 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), `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.",
|
|
1302
1372
|
"input_schema": {
|
|
1303
1373
|
"type": "object",
|
|
1304
1374
|
"properties": {
|
|
@@ -1312,7 +1382,7 @@ LOOKUP_MANIFEST: list[dict[str, Any]] = [
|
|
|
1312
1382
|
},
|
|
1313
1383
|
"input": {
|
|
1314
1384
|
"type": "object",
|
|
1315
|
-
"description": "
|
|
1385
|
+
"description": "Stored search input; healthcare_exclusions accepts {entity: person|company, release: YYYY-MM, name? | npi? | upin?, previous_release?, after?}. Exact row membership, not person identity. Give exactly one of key and input",
|
|
1316
1386
|
},
|
|
1317
1387
|
"source": {
|
|
1318
1388
|
"type": "string",
|
|
@@ -1320,7 +1390,7 @@ LOOKUP_MANIFEST: list[dict[str, Any]] = [
|
|
|
1320
1390
|
},
|
|
1321
1391
|
"since": {
|
|
1322
1392
|
"type": "string",
|
|
1323
|
-
"description": "First day of the window, YYYY-MM-DD",
|
|
1393
|
+
"description": "First day of the window, YYYY-MM-DD; optional for healthcare_exclusions monthly releases",
|
|
1324
1394
|
},
|
|
1325
1395
|
"until": {
|
|
1326
1396
|
"type": "string",
|
|
@@ -1333,7 +1403,6 @@ LOOKUP_MANIFEST: list[dict[str, Any]] = [
|
|
|
1333
1403
|
},
|
|
1334
1404
|
"required": [
|
|
1335
1405
|
"attribute",
|
|
1336
|
-
"since",
|
|
1337
1406
|
],
|
|
1338
1407
|
},
|
|
1339
1408
|
"example": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "oneshot-python"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.40.0"
|
|
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"
|
|
@@ -80,3 +80,51 @@ async def test_a_person_quote_the_credits_do_not_cover_signs_exactly_the_charge(
|
|
|
80
80
|
assert len(sent) == 2
|
|
81
81
|
assert signed.call_args.kwargs["amount"] == "0.101000"
|
|
82
82
|
assert sent[1]["x-quote-id"] == "q-person"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@pytest.mark.asyncio
|
|
86
|
+
@pytest.mark.parametrize("method", ["aperson_find", "aperson_profile", "aperson_dossier"])
|
|
87
|
+
async def test_person_quote_caps_apply_to_full_price_before_wallet_or_credit_retry(method):
|
|
88
|
+
from oneshot._errors import OneShotError
|
|
89
|
+
|
|
90
|
+
for session in ("wallet", "wallet-credits", "token"):
|
|
91
|
+
for cap in ("max_cost", "budget"):
|
|
92
|
+
if session == "token" and cap == "budget":
|
|
93
|
+
continue # Access-token budgets are configured on the server.
|
|
94
|
+
client = (OneShotClient(access_token="oneshot_" + "ab" * 32, address=MERCHANT)
|
|
95
|
+
if session == "token" else OneShotClient(TEST_PRIVATE_KEY, **(
|
|
96
|
+
{"budgets": {"per_transaction": 0.05}} if cap == "budget" else {})))
|
|
97
|
+
client._budget_synced = True
|
|
98
|
+
http = MagicMock()
|
|
99
|
+
http.post = AsyncMock(return_value=_quote("0.101000" if session == "wallet" else "0.000000"))
|
|
100
|
+
http.__aenter__ = AsyncMock(return_value=http)
|
|
101
|
+
http.__aexit__ = AsyncMock(return_value=False)
|
|
102
|
+
with patch("oneshot.client.httpx.AsyncClient", return_value=http), \
|
|
103
|
+
patch("oneshot.client.parse_payment_required", return_value={"accepted": {"amount": "0"}}), \
|
|
104
|
+
patch("oneshot.client.sign_payment_authorization") as sign:
|
|
105
|
+
with pytest.raises(OneShotError, match="exceeds"):
|
|
106
|
+
await getattr(client, method)(name="Jane", include_phone=True, **({"max_cost": 0.05} if cap == "max_cost" else {}))
|
|
107
|
+
sign.assert_not_called()
|
|
108
|
+
assert http.post.await_count == 1
|
|
109
|
+
assert "max_cost" not in http.post.call_args.kwargs["json"]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@pytest.mark.asyncio
|
|
113
|
+
@pytest.mark.parametrize("method", ["aperson_find", "aperson_profile", "aperson_dossier"])
|
|
114
|
+
async def test_person_wait_false_preserves_receipt_and_keeps_controls_out_of_quote(method):
|
|
115
|
+
for token in (False, True):
|
|
116
|
+
client = (OneShotClient(access_token="oneshot_" + "ab" * 32, address=MERCHANT)
|
|
117
|
+
if token else OneShotClient(TEST_PRIVATE_KEY))
|
|
118
|
+
client._budget_synced = True
|
|
119
|
+
http = MagicMock()
|
|
120
|
+
http.post = AsyncMock(side_effect=[_quote("0.000000"), _resp(202, {
|
|
121
|
+
"request_id": "job-1", "receipt_id": "receipt-1", "status": "processing",
|
|
122
|
+
})])
|
|
123
|
+
http.__aenter__ = AsyncMock(return_value=http)
|
|
124
|
+
http.__aexit__ = AsyncMock(return_value=False)
|
|
125
|
+
with patch("oneshot.client.httpx.AsyncClient", return_value=http), \
|
|
126
|
+
patch("oneshot.client.parse_payment_required", return_value={"accepted": {"amount": "0"}}):
|
|
127
|
+
result = await getattr(client, method)(name="Jane", include_phone=True, wait=False, wait_for_phones=True, phone_timeout_sec=1)
|
|
128
|
+
assert result["receipt_id"] == "receipt-1"
|
|
129
|
+
assert http.post.call_args.kwargs["json"] == {"name": "Jane", "include_phone": True}
|
|
130
|
+
assert http.post.call_args.kwargs["headers"]["x-quote-id"] == "q-person"
|
|
@@ -351,3 +351,9 @@ def test_dossier_profile_section_retains_strict_phone_wait_detection():
|
|
|
351
351
|
for value in (True, False, "true", None):
|
|
352
352
|
result = {"result": {"profile": {"state": "found", "data": {"phones_pending": value}}}}
|
|
353
353
|
assert OneShotClient._is_phones_pending(result) is (value is True)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def test_worker_profile_and_search_pending_shapes():
|
|
357
|
+
for value in (True, False, None, "true", 1):
|
|
358
|
+
assert OneShotClient._is_phones_pending({"status": "completed", "profile": {"phones_pending": value}}) is (value is True)
|
|
359
|
+
assert OneShotClient._is_phones_pending({"status": "completed", "results": [None, {}, {"phones_pending": value}]}) is (value is True)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""#1336: sync/async product discovery uses the new name without payload changes."""
|
|
2
|
+
from unittest.mock import AsyncMock, patch
|
|
3
|
+
import pytest
|
|
4
|
+
from oneshot import OneShotClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.asyncio
|
|
8
|
+
async def test_product_find_sync_and_async_have_no_old_alias():
|
|
9
|
+
client = OneShotClient("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")
|
|
10
|
+
result = {"products": [{"product_url": "https://shop.example/products/desk"}], "count": 1}
|
|
11
|
+
assert not hasattr(client, "commerce_search")
|
|
12
|
+
assert not hasattr(client, "acommerce_search")
|
|
13
|
+
with patch.object(OneShotClient, "call_tool", return_value=result) as call:
|
|
14
|
+
assert client.product_find("desk") is result
|
|
15
|
+
call.assert_called_once_with("/v1/tools/find/product", {"query": "desk", "limit": 10})
|
|
16
|
+
with patch.object(OneShotClient, "acall_tool", new_callable=AsyncMock, return_value=result) as call:
|
|
17
|
+
assert await client.aproduct_find("desk", limit=5) is result
|
|
18
|
+
call.assert_awaited_once_with("/v1/tools/find/product", {"query": "desk", "limit": 5})
|
|
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
|
|
File without changes
|