oneshot-python 0.35.0__tar.gz → 0.36.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 (37) hide show
  1. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/PKG-INFO +2 -2
  2. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/README.md +23 -1
  3. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/oneshot/client.py +26 -1
  4. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/pyproject.toml +2 -2
  5. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_linkedin.py +34 -0
  6. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/.gitignore +0 -0
  7. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/oneshot/__init__.py +0 -0
  8. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/oneshot/_errors.py +0 -0
  9. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/oneshot/_types.py +0 -0
  10. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/oneshot/physical_mail.py +0 -0
  11. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/oneshot/x402.py +0 -0
  12. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/__init__.py +0 -0
  13. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/conftest.py +0 -0
  14. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_access_token.py +0 -0
  15. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_approvals.py +0 -0
  16. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_balance.py +0 -0
  17. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_browser.py +0 -0
  18. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_budgets.py +0 -0
  19. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_charge_amount.py +0 -0
  20. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_compute.py +0 -0
  21. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_domains.py +0 -0
  22. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_email_payload.py +0 -0
  23. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_emergency_error.py +0 -0
  24. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_hunts.py +0 -0
  25. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_inbound_voice.py +0 -0
  26. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_max_cost_header.py +0 -0
  27. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_payment_rejection.py +0 -0
  28. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_phones_pending.py +0 -0
  29. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_physical_mail.py +0 -0
  30. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_poll_backoff.py +0 -0
  31. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_request_id.py +0 -0
  32. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_signed_job_and_analytics.py +0 -0
  33. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_tag_receipt_value.py +0 -0
  34. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_top_up_credits.py +0 -0
  35. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_wait_false.py +0 -0
  36. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/tests/test_x402.py +0 -0
  37. {oneshot_python-0.35.0 → oneshot_python-0.36.0}/uv.lock +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: oneshot-python
3
- Version: 0.35.0
4
- Summary: Core Python SDK for the OneShot API — HTTP client with x402 payment handling — 45 tools
3
+ Version: 0.36.0
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
7
7
  Requires-Dist: eth-account>=0.13.0
@@ -269,6 +269,28 @@ client.linkedin_withdraw_invitation(
269
269
 
270
270
  Async equivalents are `alinkedin_invite()` and `alinkedin_withdraw_invitation()`. Use the OneShot `invitation_id`, not the provider invitation ID. Withdrawal can cancel a queued invitation and never disconnects an established connection. Invitations default to 25 per account per UTC day; withdrawal does not restore capacity for an invitation already sent. Reuse the same key when retrying an operation.
271
271
 
272
+ ### LinkedIn: message a connection and read invitation status
273
+
274
+ Available in `oneshot-python` 0.36.0 and later.
275
+
276
+ ```python
277
+ # Accepted as soon as the person is a connection; no message needs to be exchanged first.
278
+ current = client.linkedin_invitation(account_id, invitation["invitation_id"])
279
+ if current["status"] == "accepted":
280
+ sent = client.linkedin_message(
281
+ account_id,
282
+ "https://www.linkedin.com/in/jane-doe/",
283
+ "Thanks for connecting.",
284
+ idempotency_key="event-jane-first-message",
285
+ )
286
+ conversation_id = sent["conversation_id"]
287
+
288
+ # Poll for changes instead of asking one by one:
289
+ changed = client.linkedin_invitations(account_id, status="accepted", changed_since="2026-10-01T00:00:00Z")
290
+ ```
291
+
292
+ `linkedin_message()` posts into the 1:1 conversation the account already has with that connection, or starts one. It needs the `reply` grant and shares the reply daily cap. A failed send carries an error code: `not_connected` (invite them first) and `rate_limited` mean defer, not give up. The two invitation reads are free. Statuses are `pending`, `accepted`, `withdrawn`, `declined_or_unknown` and `not_sent`. Async equivalents are `alinkedin_message()`, `alinkedin_invitation()` and `alinkedin_invitations()`.
293
+
272
294
  ### Soul Hunt
273
295
 
274
296
  `client.hunts` talks to Soul Hunt (`api.soulhunt.ai` by default — override with `OneShotClient(..., hunt_url=...)`), a separate service from the main OneShot API:
@@ -301,7 +323,7 @@ Async variants prefix the method with `a` (`alist`, `astatus`, `apredict`, `acap
301
323
  ## Links
302
324
 
303
325
  - [Documentation](https://docs.oneshotagent.com/sdk/installation#install-via-pip-python)
304
- - [LangChain integration](https://pypi.org/project/langchain-oneshot/): 45 tools as LangChain BaseTool
326
+ - [LangChain integration](https://pypi.org/project/langchain-oneshot/): 48 tools as LangChain BaseTool
305
327
  - [GAME plugin](https://pypi.org/project/game-plugin-oneshot/): Virtuals Protocol integration
306
328
  - [TypeScript SDK](https://www.npmjs.com/package/@oneshot-agent/sdk)
307
329
  - [MCP Server](https://www.npmjs.com/package/@oneshot-agent/mcp-server)
@@ -59,7 +59,7 @@ try:
59
59
 
60
60
  SDK_VERSION = _pkg_version("oneshot-python")
61
61
  except Exception: # pragma: no cover - editable/source runs without dist metadata
62
- SDK_VERSION = "0.35.0"
62
+ SDK_VERSION = "0.36.0"
63
63
 
64
64
  _BASE_URL = "https://win.oneshotagent.com"
65
65
  _HUNT_BASE_URL = "https://api.soulhunt.ai"
@@ -1667,6 +1667,20 @@ class OneShotClient:
1667
1667
  async def alinkedin_messages(self, account_id: str, **params: Any) -> dict:
1668
1668
  return await self.acall_free_get(f"/v1/tools/linkedin/accounts/{quote(account_id, safe='')}/messages", _linkedin_query(params))
1669
1669
 
1670
+ def linkedin_invitation(self, account_id: str, invitation_id: str) -> dict:
1671
+ """One invitation sent through linkedin_invite: `accepted` (with `accepted_at`) as soon as the person is a connection. Free."""
1672
+ return self.call_free_get(f"/v1/tools/linkedin/accounts/{quote(account_id, safe='')}/invitations/{quote(invitation_id, safe='')}")
1673
+
1674
+ async def alinkedin_invitation(self, account_id: str, invitation_id: str) -> dict:
1675
+ return await self.acall_free_get(f"/v1/tools/linkedin/accounts/{quote(account_id, safe='')}/invitations/{quote(invitation_id, safe='')}")
1676
+
1677
+ def linkedin_invitations(self, account_id: str, **params: Any) -> dict:
1678
+ """Invitations sent through linkedin_invite, most recently changed first. Params: status, changed_since, limit. Free."""
1679
+ return self.call_free_get(f"/v1/tools/linkedin/accounts/{quote(account_id, safe='')}/invitations", _linkedin_query(params))
1680
+
1681
+ async def alinkedin_invitations(self, account_id: str, **params: Any) -> dict:
1682
+ return await self.acall_free_get(f"/v1/tools/linkedin/accounts/{quote(account_id, safe='')}/invitations", _linkedin_query(params))
1683
+
1670
1684
  def _linkedin_invitation_call(self, endpoint: str, payload: dict, **kwargs: Any) -> Any:
1671
1685
  return asyncio.get_event_loop().run_until_complete(self._alinkedin_invitation_call(endpoint, payload, **kwargs))
1672
1686
 
@@ -1713,6 +1727,17 @@ class OneShotClient:
1713
1727
  return await self.acall_tool("/v1/tools/linkedin/reply", {"account_id": account_id, "conversation_id": conversation_id, "text": text, **kwargs},
1714
1728
  max_cost=max_cost, timeout_sec=timeout_sec, wait=wait, idempotency_key=idempotency_key or _new_idempotency_key())
1715
1729
 
1730
+ def linkedin_message(self, account_id: str, profile: str, text: str, *, idempotency_key: Optional[str] = None, max_cost: Optional[float] = None, timeout_sec: int = 180, wait: bool = True, **kwargs: Any) -> Any:
1731
+ """Message a first-degree connection: into the 1:1 conversation the account already has with them, or a new one.
1732
+ Needs the `reply` grant and shares its daily cap. A failed send carries an error code: `not_connected` and
1733
+ `rate_limited` mean defer, not give up. An Idempotency-Key is generated when not supplied. Blocking."""
1734
+ return self.call_tool("/v1/tools/linkedin/message", {"account_id": account_id, "profile": profile, "text": text, **kwargs},
1735
+ max_cost=max_cost, timeout_sec=timeout_sec, wait=wait, idempotency_key=idempotency_key or _new_idempotency_key())
1736
+
1737
+ async def alinkedin_message(self, account_id: str, profile: str, text: str, *, idempotency_key: Optional[str] = None, max_cost: Optional[float] = None, timeout_sec: int = 180, wait: bool = True, **kwargs: Any) -> Any:
1738
+ return await self.acall_tool("/v1/tools/linkedin/message", {"account_id": account_id, "profile": profile, "text": text, **kwargs},
1739
+ max_cost=max_cost, timeout_sec=timeout_sec, wait=wait, idempotency_key=idempotency_key or _new_idempotency_key())
1740
+
1716
1741
  def linkedin_view_profile(self, account_id: str, identifier: str, *, notify: bool = False, idempotency_key: Optional[str] = None, max_cost: Optional[float] = None, timeout_sec: int = 180, wait: bool = True, **kwargs: Any) -> Any:
1717
1742
  """View a profile through the connected account (public-safe summary). notify=True cannot be undone. Blocking."""
1718
1743
  return self.call_tool("/v1/tools/linkedin/profile-view", {"account_id": account_id, "identifier": identifier, "notify": bool(notify), **kwargs},
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "oneshot-python"
3
- version = "0.35.0"
4
- description = "Core Python SDK for the OneShot API — HTTP client with x402 payment handling — 45 tools"
3
+ version = "0.36.0"
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"
7
7
  requires-python = ">=3.10"
@@ -187,3 +187,37 @@ def test_reconnect_sends_complete_upgrade_grant(asynchronous):
187
187
  client.call_free_post = Mock(return_value=result)
188
188
  assert client.reconnect_linkedin_account('account/id', requested_actions=actions) == result
189
189
  client.call_free_post.assert_called_once_with('/v1/tools/linkedin/accounts/account%2Fid/reconnect', {'requested_actions': actions}, scope='write')
190
+
191
+
192
+ @pytest.mark.asyncio
193
+ async def test_message_is_paid_and_idempotency_keyed():
194
+ client = OneShotClient(TEST_PRIVATE_KEY)
195
+ client.acall_tool = AsyncMock(return_value={"status": "sent", "conversation_id": "conv"})
196
+
197
+ await client.alinkedin_message("acc", "https://www.linkedin.com/in/jane/", "hi", memo="m")
198
+ args, kwargs = client.acall_tool.await_args
199
+ assert args == ("/v1/tools/linkedin/message", {"account_id": "acc", "profile": "https://www.linkedin.com/in/jane/", "text": "hi", "memo": "m"})
200
+ assert len(kwargs["idempotency_key"]) == 32 # auto-generated
201
+ assert kwargs["timeout_sec"] == 180 and kwargs["wait"] is True
202
+
203
+ await client.alinkedin_message("acc", "jane", "hi", idempotency_key="fixed")
204
+ assert client.acall_tool.await_args.kwargs["idempotency_key"] == "fixed"
205
+
206
+
207
+ @pytest.mark.asyncio
208
+ async def test_invitation_status_reads_are_free():
209
+ client = OneShotClient(TEST_PRIVATE_KEY)
210
+ client.acall_free_get = AsyncMock(return_value={"invitations": []})
211
+ client.acall_tool = AsyncMock()
212
+
213
+ await client.alinkedin_invitation("acc", "inv 1")
214
+ client.acall_free_get.assert_awaited_with("/v1/tools/linkedin/accounts/acc/invitations/inv%201")
215
+ await client.alinkedin_invitations("acc", status="accepted", changed_since="2026-10-01T00:00:00Z", limit=20)
216
+ client.acall_free_get.assert_awaited_with(
217
+ "/v1/tools/linkedin/accounts/acc/invitations",
218
+ {"status": "accepted", "changed_since": "2026-10-01T00:00:00Z", "limit": "20"},
219
+ )
220
+ await client.alinkedin_invitations("acc")
221
+ client.acall_free_get.assert_awaited_with("/v1/tools/linkedin/accounts/acc/invitations", None)
222
+ client.acall_tool.assert_not_awaited()
223
+
File without changes