oneshot-python 0.38.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.
Files changed (51) hide show
  1. oneshot_python-0.40.0/CHANGELOG.md +7 -0
  2. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/PKG-INFO +1 -1
  3. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/README.md +16 -1
  4. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/oneshot/client.py +98 -72
  5. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/oneshot/lookup_manifest.py +318 -131
  6. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/pyproject.toml +1 -1
  7. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_declared_routes.py +4 -4
  8. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_lookup.py +8 -0
  9. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_paid_call_proof.py +1 -1
  10. oneshot_python-0.40.0/tests/test_person_quote_credits.py +130 -0
  11. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_phones_pending.py +13 -1
  12. oneshot_python-0.40.0/tests/test_product_find.py +18 -0
  13. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/.gitignore +0 -0
  14. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/oneshot/__init__.py +0 -0
  15. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/oneshot/_errors.py +0 -0
  16. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/oneshot/_types.py +0 -0
  17. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/oneshot/lookup.py +0 -0
  18. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/oneshot/physical_mail.py +0 -0
  19. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/oneshot/x402.py +0 -0
  20. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/__init__.py +0 -0
  21. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/conftest.py +0 -0
  22. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_access_token.py +0 -0
  23. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_approvals.py +0 -0
  24. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_balance.py +0 -0
  25. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_browser.py +0 -0
  26. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_budgets.py +0 -0
  27. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_charge_amount.py +0 -0
  28. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_compute.py +0 -0
  29. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_compute_manifest.py +0 -0
  30. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_compute_outputs.py +0 -0
  31. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_declared_worker_reads.py +0 -0
  32. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_domains.py +0 -0
  33. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_domains_signed.py +0 -0
  34. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_email_payload.py +0 -0
  35. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_emergency_error.py +0 -0
  36. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_hunts.py +0 -0
  37. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_inbound_voice.py +0 -0
  38. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_inbox_notifications.py +0 -0
  39. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_linkedin.py +0 -0
  40. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_max_cost_header.py +0 -0
  41. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_payment_rejection.py +0 -0
  42. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_physical_mail.py +0 -0
  43. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_poll_backoff.py +0 -0
  44. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_request_id.py +0 -0
  45. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_signed_job_and_analytics.py +0 -0
  46. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_tag_receipt_value.py +0 -0
  47. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_top_up_credits.py +0 -0
  48. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_update_build.py +0 -0
  49. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_wait_false.py +0 -0
  50. {oneshot_python-0.38.0 → oneshot_python-0.40.0}/tests/test_x402.py +0 -0
  51. {oneshot_python-0.38.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: oneshot-python
3
- Version: 0.38.0
3
+ Version: 0.40.0
4
4
  Summary: Core Python SDK for the OneShot API — HTTP client with x402 payment handling — 48 tools
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
@@ -244,7 +244,7 @@ except LinkedInConnectRequiredError:
244
244
 
245
245
  Account reads stop offering reconnect after confirming upstream deletion.
246
246
  Missing accounts are excluded from the active list; `include_revoked=True`
247
- lets you inspect their `deleted_upstream` status and `reconnect_required=False`.
247
+ lets you inspect their `connection_deleted` status and `reconnect_required=False`.
248
248
  Authentication failures and timeouts remain ordinary errors and do not instruct
249
249
  the consumer to create a new connection.
250
250
 
@@ -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,13 +60,13 @@ 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.38.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"
67
67
  _ACCESS_TOKEN_RE = re.compile(r"^oneshot_[0-9a-f]{64}$")
68
68
  # Endpoints whose admission reads a "submit"-scoped agent proof (same list as the TypeScript SDK).
69
- _DURABLE_ADMISSION_ENDPOINT = re.compile(r"(enrich/(profile|email)|verify/email|physical-mail/send)$")
69
+ _DURABLE_ADMISSION_ENDPOINT = re.compile(r"(lookup/person-(profile|email|email-status)|lookup/place-permits|physical-mail/send)$")
70
70
  _CHAIN_ID = 8453
71
71
  _USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
72
72
 
@@ -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,37 +2077,49 @@ 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
- def people_search(self, **kwargs: Any) -> Any:
2077
- """Search for people by job title, company, etc. Blocking."""
2078
- return self.call_tool("/v1/tools/research/people", kwargs)
2079
-
2080
- async def apeople_search(self, **kwargs: Any) -> Any:
2081
- """Search for people by job title, company, etc. Async."""
2082
- return await self.acall_tool("/v1/tools/research/people", kwargs)
2083
-
2084
- def enrich_profile(self, **kwargs: Any) -> Any:
2085
- """Enrich a person's profile from LinkedIn URL, email, or name. Blocking."""
2086
- return self.call_tool("/v1/tools/enrich/profile", kwargs)
2087
-
2088
- async def aenrich_profile(self, **kwargs: Any) -> Any:
2089
- """Enrich a person's profile from LinkedIn URL, email, or name. Async."""
2090
- return await self.acall_tool("/v1/tools/enrich/profile", kwargs)
2091
-
2092
- def company_search(self, **kwargs: Any) -> Any:
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)
2107
+
2108
+ def company_find(self, **kwargs: Any) -> Any:
2093
2109
  """Search for companies by name, domain, industry, location, size, etc. Blocking."""
2094
- return self.call_tool("/v1/tools/research/company", kwargs)
2110
+ return self.call_tool("/v1/tools/find/company", kwargs)
2095
2111
 
2096
- async def acompany_search(self, **kwargs: Any) -> Any:
2112
+ async def acompany_find(self, **kwargs: Any) -> Any:
2097
2113
  """Search for companies by name, domain, industry, location, size, etc. Async."""
2098
- return await self.acall_tool("/v1/tools/research/company", kwargs)
2114
+ return await self.acall_tool("/v1/tools/find/company", kwargs)
2099
2115
 
2100
- def enrich_company(self, **kwargs: Any) -> Any:
2116
+ def company_profile(self, **kwargs: Any) -> Any:
2101
2117
  """Enrich a company from domain, name, LinkedIn URL, or ticker. Blocking."""
2102
- return self.call_tool("/v1/tools/enrich/company", kwargs)
2118
+ return self.call_tool("/v1/tools/lookup/company-profile", kwargs)
2103
2119
 
2104
- async def aenrich_company(self, **kwargs: Any) -> Any:
2120
+ async def acompany_profile(self, **kwargs: Any) -> Any:
2105
2121
  """Enrich a company from domain, name, LinkedIn URL, or ticker. Async."""
2106
- return await self.acall_tool("/v1/tools/enrich/company", kwargs)
2122
+ return await self.acall_tool("/v1/tools/lookup/company-profile", kwargs)
2107
2123
 
2108
2124
  def business_find(self, **kwargs: Any) -> Any:
2109
2125
  """Discover local businesses (restaurants, contractors, practices) by category/keywords x location. Blocking."""
@@ -2129,7 +2145,7 @@ class OneShotClient:
2129
2145
  """Federal Sources Sought / Presolicitation notices (SAM.gov) by NAICS code with the contracting officer's contact. Result carries `data_as_of` (snapshot behind the rows). Async."""
2130
2146
  return await self.acall_tool("/v1/tools/find/record-solicitations", {"naics": naics, **kwargs})
2131
2147
 
2132
- def find_email(self, company_domain: str, *, full_name: Optional[str] = None, first_name: Optional[str] = None, last_name: Optional[str] = None, **kwargs: Any) -> Any:
2148
+ def person_email(self, company_domain: str, *, full_name: Optional[str] = None, first_name: Optional[str] = None, last_name: Optional[str] = None, **kwargs: Any) -> Any:
2133
2149
  """Find a person's email address. Blocking."""
2134
2150
  payload: dict[str, Any] = {"company_domain": company_domain, **kwargs}
2135
2151
  if full_name:
@@ -2138,9 +2154,9 @@ class OneShotClient:
2138
2154
  payload["first_name"] = first_name
2139
2155
  if last_name:
2140
2156
  payload["last_name"] = last_name
2141
- return self.call_tool("/v1/tools/enrich/email", payload)
2157
+ return self.call_tool("/v1/tools/lookup/person-email", payload)
2142
2158
 
2143
- async def afind_email(self, company_domain: str, *, full_name: Optional[str] = None, first_name: Optional[str] = None, last_name: Optional[str] = None, **kwargs: Any) -> Any:
2159
+ async def aperson_email(self, company_domain: str, *, full_name: Optional[str] = None, first_name: Optional[str] = None, last_name: Optional[str] = None, **kwargs: Any) -> Any:
2144
2160
  """Find a person's email address. Async."""
2145
2161
  payload: dict[str, Any] = {"company_domain": company_domain, **kwargs}
2146
2162
  if full_name:
@@ -2149,71 +2165,73 @@ class OneShotClient:
2149
2165
  payload["first_name"] = first_name
2150
2166
  if last_name:
2151
2167
  payload["last_name"] = last_name
2152
- return await self.acall_tool("/v1/tools/enrich/email", payload)
2168
+ return await self.acall_tool("/v1/tools/lookup/person-email", payload)
2153
2169
 
2154
- def verify_email(self, email: str) -> Any:
2170
+ def person_email_status(self, email: str) -> Any:
2155
2171
  """Verify email deliverability. Blocking."""
2156
- return self.call_tool("/v1/tools/verify/email", {"email": email})
2172
+ return self.call_tool("/v1/tools/lookup/person-email-status", {"email": email})
2157
2173
 
2158
- async def averify_email(self, email: str) -> Any:
2174
+ async def aperson_email_status(self, email: str) -> Any:
2159
2175
  """Verify email deliverability. Async."""
2160
- return await self.acall_tool("/v1/tools/verify/email", {"email": email})
2176
+ return await self.acall_tool("/v1/tools/lookup/person-email-status", {"email": email})
2161
2177
 
2162
- def deep_research_person(self, **kwargs: Any) -> Any:
2163
- """Deep research on a person. Blocking."""
2164
- return self.call_tool("/v1/tools/research/person", kwargs)
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 adeep_research_person(self, **kwargs: Any) -> Any:
2167
- """Deep research on a person. Async."""
2168
- return await self.acall_tool("/v1/tools/research/person", kwargs)
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
- def social_profiles(self, **kwargs: Any) -> Any:
2188
+ def person_social_accounts(self, **kwargs: Any) -> Any:
2171
2189
  """Discover social media profiles. Blocking."""
2172
- return self.call_tool("/v1/tools/research/social", kwargs)
2190
+ return self.call_tool("/v1/tools/lookup/person-social-accounts", kwargs)
2173
2191
 
2174
- async def asocial_profiles(self, **kwargs: Any) -> Any:
2192
+ async def aperson_social_accounts(self, **kwargs: Any) -> Any:
2175
2193
  """Discover social media profiles. Async."""
2176
- return await self.acall_tool("/v1/tools/research/social", kwargs)
2194
+ return await self.acall_tool("/v1/tools/lookup/person-social-accounts", kwargs)
2177
2195
 
2178
- def article_search(self, name: str, company: str, **kwargs: Any) -> Any:
2196
+ def person_articles(self, name: str, company: str, **kwargs: Any) -> Any:
2179
2197
  """Find articles by or about a person. Blocking."""
2180
- return self.call_tool("/v1/tools/research/articles", {"name": name, "company": company, **kwargs})
2198
+ return self.call_tool("/v1/tools/lookup/person-articles", {"name": name, "company": company, **kwargs})
2181
2199
 
2182
- async def aarticle_search(self, name: str, company: str, **kwargs: Any) -> Any:
2200
+ async def aperson_articles(self, name: str, company: str, **kwargs: Any) -> Any:
2183
2201
  """Find articles by or about a person. Async."""
2184
- return await self.acall_tool("/v1/tools/research/articles", {"name": name, "company": company, **kwargs})
2202
+ return await self.acall_tool("/v1/tools/lookup/person-articles", {"name": name, "company": company, **kwargs})
2185
2203
 
2186
- def person_newsfeed(self, social_media_url: str, **kwargs: Any) -> Any:
2204
+ def person_posts(self, social_media_url: str, **kwargs: Any) -> Any:
2187
2205
  """Get recent social posts from a person. Blocking."""
2188
- return self.call_tool("/v1/tools/research/newsfeed", {"social_media_url": social_media_url, **kwargs})
2206
+ return self.call_tool("/v1/tools/lookup/person-posts", {"social_media_url": social_media_url, **kwargs})
2189
2207
 
2190
- async def aperson_newsfeed(self, social_media_url: str, **kwargs: Any) -> Any:
2208
+ async def aperson_posts(self, social_media_url: str, **kwargs: Any) -> Any:
2191
2209
  """Get recent social posts from a person. Async."""
2192
- return await self.acall_tool("/v1/tools/research/newsfeed", {"social_media_url": social_media_url, **kwargs})
2210
+ return await self.acall_tool("/v1/tools/lookup/person-posts", {"social_media_url": social_media_url, **kwargs})
2193
2211
 
2194
2212
  def person_interests(self, **kwargs: Any) -> Any:
2195
2213
  """Analyze a person's interests from their digital footprint. Blocking."""
2196
- return self.call_tool("/v1/tools/research/interests", kwargs)
2214
+ return self.call_tool("/v1/tools/lookup/person-interests", kwargs)
2197
2215
 
2198
2216
  async def aperson_interests(self, **kwargs: Any) -> Any:
2199
2217
  """Analyze a person's interests from their digital footprint. Async."""
2200
- return await self.acall_tool("/v1/tools/research/interests", kwargs)
2218
+ return await self.acall_tool("/v1/tools/lookup/person-interests", kwargs)
2201
2219
 
2202
- def person_interactions(self, social_media_url: str, **kwargs: Any) -> Any:
2220
+ def person_followers(self, social_media_url: str, **kwargs: Any) -> Any:
2203
2221
  """Get a person's social interactions. Blocking."""
2204
- return self.call_tool("/v1/tools/research/interactions", {"social_media_url": social_media_url, **kwargs})
2222
+ return self.call_tool("/v1/tools/lookup/person-followers", {"social_media_url": social_media_url, **kwargs})
2205
2223
 
2206
- async def aperson_interactions(self, social_media_url: str, **kwargs: Any) -> Any:
2224
+ async def aperson_followers(self, social_media_url: str, **kwargs: Any) -> Any:
2207
2225
  """Get a person's social interactions. Async."""
2208
- return await self.acall_tool("/v1/tools/research/interactions", {"social_media_url": social_media_url, **kwargs})
2226
+ return await self.acall_tool("/v1/tools/lookup/person-followers", {"social_media_url": social_media_url, **kwargs})
2209
2227
 
2210
- def commerce_search(self, query: str, *, limit: int = 10, **kwargs: Any) -> Any:
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/commerce/search", {"query": query, "limit": limit, **kwargs})
2230
+ return self.call_tool("/v1/tools/find/product", {"query": query, "limit": limit, **kwargs})
2213
2231
 
2214
- async def acommerce_search(self, query: str, *, limit: int = 10, **kwargs: Any) -> Any:
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/commerce/search", {"query": query, "limit": limit, **kwargs})
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,19 +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. Tolerates two shapes: the unwrapped result ``{phones_pending}``
2519
- and the deep_research_person wrapper ``{result: {phones_pending}}``.
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
- if result.get("phones_pending") is True:
2527
- return True
2528
- inner = result.get("result")
2529
- if isinstance(inner, dict) and inner.get("phones_pending") is True:
2530
- return True
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
2531
2557
  return False
2532
2558
 
2533
2559
  async def _poll_for_phones(