oneshot-python 0.18.0__tar.gz → 0.20.2__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 (27) hide show
  1. {oneshot_python-0.18.0 → oneshot_python-0.20.2}/.gitignore +3 -0
  2. {oneshot_python-0.18.0 → oneshot_python-0.20.2}/PKG-INFO +1 -1
  3. {oneshot_python-0.18.0 → oneshot_python-0.20.2}/README.md +37 -0
  4. {oneshot_python-0.18.0 → oneshot_python-0.20.2}/oneshot/__init__.py +10 -0
  5. oneshot_python-0.20.2/oneshot/_errors.py +129 -0
  6. {oneshot_python-0.18.0 → oneshot_python-0.20.2}/oneshot/_types.py +40 -0
  7. {oneshot_python-0.18.0 → oneshot_python-0.20.2}/oneshot/client.py +382 -21
  8. {oneshot_python-0.18.0 → oneshot_python-0.20.2}/pyproject.toml +1 -1
  9. oneshot_python-0.20.2/tests/test_budgets.py +184 -0
  10. oneshot_python-0.20.2/tests/test_charge_amount.py +57 -0
  11. oneshot_python-0.20.2/tests/test_payment_rejection.py +88 -0
  12. oneshot_python-0.20.2/tests/test_poll_backoff.py +107 -0
  13. {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/test_request_id.py +1 -4
  14. oneshot_python-0.20.2/tests/test_wait_false.py +88 -0
  15. oneshot_python-0.18.0/oneshot/_errors.py +0 -56
  16. {oneshot_python-0.18.0 → oneshot_python-0.20.2}/oneshot/x402.py +0 -0
  17. {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/__init__.py +0 -0
  18. {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/test_balance.py +0 -0
  19. {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/test_compute.py +0 -0
  20. {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/test_domains.py +0 -0
  21. {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/test_email_payload.py +0 -0
  22. {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/test_emergency_error.py +0 -0
  23. {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/test_max_cost_header.py +0 -0
  24. {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/test_phones_pending.py +0 -0
  25. {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/test_tag_receipt_value.py +0 -0
  26. {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/test_x402.py +0 -0
  27. {oneshot_python-0.18.0 → oneshot_python-0.20.2}/uv.lock +0 -0
@@ -90,3 +90,6 @@ output/
90
90
 
91
91
  # Claude Code agent metadata (lockfile + local settings)
92
92
  .claude/
93
+
94
+ # generated by scripts/deploy-blog-generator.sh
95
+ scripts/humanizer.vendored.ts
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: oneshot-python
3
- Version: 0.18.0
3
+ Version: 0.20.2
4
4
  Summary: Core Python SDK for the OneShot API — HTTP client with x402 payment handling
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
@@ -66,6 +66,43 @@ Read endpoints (inbox, SMS inbox, notifications, balance, browser profiles) retu
66
66
 
67
67
  This is automatic — no extra code beyond `private_key`. **Requires `oneshot-python >= 0.17.0`.** Older versions keep working until the API enables enforcement, after which they must send a valid `x-agent-proof`.
68
68
 
69
+ ## Spend Budgets
70
+
71
+ Cap what the agent can spend so a runaway loop can't drain the wallet overnight:
72
+
73
+ ```python
74
+ client = OneShotClient(
75
+ "0x...",
76
+ budgets={
77
+ "daily": 50, # max USDC per UTC day
78
+ "per_transaction": 5, # max USDC for any single call
79
+ "alert_at": 0.8, # warn at 80% of daily
80
+ "pause_at": 1.0, # stop paying at 100%
81
+ },
82
+ alert_email="you@example.com",
83
+ )
84
+ ```
85
+
86
+ Unlike `max_cost` (a per-call client-side check), budgets are **enforced server-side** against your receipt ledger. The config is synced once before your first paid call, so the daily figure is a true per-agent total — shared across every process and restart using the same wallet.
87
+
88
+ At `alert_at` you get a `budget_warning` notification (in-app via `/v1/tools/notifications`, plus email if set). At `pause_at` paid calls raise `BudgetExceededError` until the window resets at the next UTC midnight:
89
+
90
+ ```python
91
+ from oneshot import BudgetExceededError
92
+
93
+ try:
94
+ client.research(topic="...")
95
+ except BudgetExceededError as e:
96
+ print(f"{e.reason} budget hit — ${e.spent} spent, resumes {e.resets_at}")
97
+
98
+ client.get_budgets()
99
+ # {"daily_usdc": 50, "spent_today_usdc": "41.20", "pct_used": 0.824, "resets_at": "..."}
100
+ ```
101
+
102
+ Omit `budgets` to leave whatever is stored server-side untouched — agents without a budget are unlimited. **Requires `oneshot-python >= 0.20.0`.**
103
+
104
+ The sync **fails closed**: if the budget can't be confirmed with the server, the paid call raises `BudgetSyncError` and is not made (retried on the next call). An invalid config (`daily=-1`, `alert_at=2`) raises `ValidationError` at construction.
105
+
69
106
  ## Configuration
70
107
 
71
108
  ```python
@@ -1,15 +1,20 @@
1
1
  """oneshot-python — Core Python SDK for the OneShot API."""
2
2
 
3
3
  from oneshot._errors import (
4
+ BudgetExceededError,
5
+ BudgetSyncError,
4
6
  ContentBlockedError,
5
7
  EmergencyNumberError,
6
8
  JobError,
7
9
  JobTimeoutError,
8
10
  OneShotError,
11
+ PaymentError,
9
12
  ToolError,
10
13
  ValidationError,
11
14
  )
12
15
  from oneshot._types import (
16
+ AgentBudgetConfig,
17
+ AgentBudgetStatus,
13
18
  ComputeBudgetStatus,
14
19
  ComputeCancelResult,
15
20
  ComputeFundResult,
@@ -32,12 +37,17 @@ __all__ = [
32
37
  "OneShotClient",
33
38
  "OneShotError",
34
39
  "ToolError",
40
+ "PaymentError",
41
+ "BudgetExceededError",
42
+ "BudgetSyncError",
35
43
  "JobError",
36
44
  "JobTimeoutError",
37
45
  "ValidationError",
38
46
  "ContentBlockedError",
39
47
  "EmergencyNumberError",
40
48
  "sign_payment_authorization",
49
+ "AgentBudgetConfig",
50
+ "AgentBudgetStatus",
41
51
  "ComputeSchedule",
42
52
  "ComputeGoalResult",
43
53
  "ComputeGoalStatus",
@@ -0,0 +1,129 @@
1
+ """Error classes mirroring the TypeScript OneShot SDK error hierarchy."""
2
+
3
+
4
+ class OneShotError(Exception):
5
+ """Base error for all OneShot operations."""
6
+
7
+
8
+ class ToolError(OneShotError):
9
+ """HTTP-level error from the OneShot API."""
10
+
11
+ def __init__(self, message: str, status_code: int, response_body: str) -> None:
12
+ super().__init__(message)
13
+ self.status_code = status_code
14
+ self.response_body = response_body
15
+
16
+
17
+ class PaymentError(OneShotError):
18
+ """The facilitator rejected the payment signed for this request.
19
+
20
+ Distinct from the ordinary 402 that opens the quote-pay handshake: this is a
21
+ 402 arriving on the PAID retry, meaning the signature was refused.
22
+ ``reason`` is the facilitator's machine-readable code
23
+ (``insufficient_funds``, ``invalid_exact_evm_payload_authorization_value``,
24
+ …) and ``expected`` / ``received`` name the amounts when the two disagree —
25
+ the case that produced a silent, bodiless 402 before the server started
26
+ reporting it.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ message: str,
32
+ reason: str,
33
+ expected: dict | None = None,
34
+ received: dict | None = None,
35
+ quote_id: str | None = None,
36
+ ) -> None:
37
+ super().__init__(message)
38
+ self.reason = reason
39
+ self.expected = expected or {}
40
+ self.received = received or {}
41
+ self.quote_id = quote_id
42
+
43
+
44
+ class JobError(OneShotError):
45
+ """Async job completed with an error."""
46
+
47
+ def __init__(self, message: str, job_id: str, job_error: str) -> None:
48
+ super().__init__(message)
49
+ self.job_id = job_id
50
+ self.job_error = job_error
51
+
52
+
53
+ class JobTimeoutError(OneShotError):
54
+ """Async job exceeded the polling timeout."""
55
+
56
+ def __init__(self, job_id: str, elapsed_ms: int) -> None:
57
+ super().__init__(f"Job {job_id} timed out after {elapsed_ms / 1000}s")
58
+ self.job_id = job_id
59
+ self.elapsed_ms = elapsed_ms
60
+
61
+
62
+ class ValidationError(OneShotError):
63
+ """Client-side input validation failure."""
64
+
65
+ def __init__(self, message: str, field: str) -> None:
66
+ super().__init__(message)
67
+ self.field = field
68
+
69
+
70
+ class ContentBlockedError(OneShotError):
71
+ """Content safety filter rejected the request."""
72
+
73
+ def __init__(self, message: str, categories: list[str]) -> None:
74
+ super().__init__(message)
75
+ self.categories = categories
76
+
77
+
78
+ class EmergencyNumberError(OneShotError):
79
+ """Voice call rejected because the target number is an emergency line."""
80
+
81
+ def __init__(self, message: str, blocked_number: str) -> None:
82
+ super().__init__(message)
83
+ self.blocked_number = blocked_number
84
+
85
+
86
+ class BudgetExceededError(OneShotError):
87
+ """The agent's own spend budget stopped this call (HTTP 403 ``budget_exceeded``).
88
+
89
+ Distinct from :class:`PaymentError`: nothing was signed and nothing was
90
+ charged. A retry cannot succeed until the daily window resets
91
+ (``resets_at``) or the budget is raised — which is why the server answers
92
+ 403 rather than 402. ``reason`` is ``"daily"`` when cumulative UTC-day
93
+ spend would cross the budget, or ``"per_transaction"`` when this single
94
+ call is larger than the per-call cap.
95
+
96
+ Mirrors ``BudgetExceededError`` in ``libs/agent-sdk/src/errors.ts``.
97
+ """
98
+
99
+ def __init__(
100
+ self,
101
+ message: str,
102
+ reason: str,
103
+ cap: float | None = None,
104
+ spent: float | None = None,
105
+ charge: float | None = None,
106
+ resets_at: str | None = None,
107
+ ) -> None:
108
+ super().__init__(message)
109
+ self.reason = reason
110
+ self.cap = cap
111
+ self.spent = spent
112
+ self.charge = charge
113
+ self.resets_at = resets_at
114
+
115
+
116
+ class BudgetSyncError(OneShotError):
117
+ """The configured spend budget could not be confirmed with the server, so
118
+ the paid call was NOT made.
119
+
120
+ Budgets are enforced server-side; if the sync fails (network error, 5xx,
121
+ rate limit, rejected config) proceeding would silently drop the guardrail
122
+ the developer configured. The sync is retried on the next paid call.
123
+ ``status`` is the HTTP status when the server answered, else None.
124
+ """
125
+
126
+ def __init__(self, message: str, status: int | None = None, body: str | None = None) -> None:
127
+ super().__init__(message)
128
+ self.status = status
129
+ self.body = body
@@ -181,6 +181,12 @@ class DomainPoolEntry(TypedDict, total=False):
181
181
 
182
182
  domain: str
183
183
  default_from: str
184
+ # 'mailbox' = a real mailbox is provisioned per address, so every address not
185
+ # already active in addresses[] bills the one-time mailbox_provisioning_fee on
186
+ # its first send — the canonical default_from included. 'relay' = header-only,
187
+ # no per-address fee. A property of the DOMAIN, fixed at setup: omitting
188
+ # mailbox_mode on a quote does not make the send relay.
189
+ mailbox_mode: str # relay | mailbox
184
190
  addresses: list[DomainAddressEntry]
185
191
  pool_status: str # active | paused | removed
186
192
  warmup_state: str # warming | warmed | degraded
@@ -227,3 +233,37 @@ __all__ = [
227
233
  "DomainPoolListResult",
228
234
  "DomainPoolStatusResult",
229
235
  ]
236
+
237
+
238
+ # ── Agent spend budgets ──────────────────────────────────────────────────
239
+
240
+
241
+ class AgentBudgetConfig(TypedDict, total=False):
242
+ """Spend budget passed to ``OneShotClient(budgets=...)``.
243
+
244
+ Mirrors ``AgentBudgetConfig`` in TS (snake_case here; camelCase keys are
245
+ accepted too and normalised). Synced to the server once, before the first
246
+ paid call, and enforced there against the receipt ledger.
247
+ """
248
+
249
+ daily: float
250
+ """Max USDC per UTC day."""
251
+ per_transaction: float
252
+ """Max USDC for any single call."""
253
+ alert_at: float
254
+ """Fraction of ``daily`` that triggers a ``budget_warning`` (default 0.8)."""
255
+ pause_at: float
256
+ """Fraction of ``daily`` at which paid calls stop (default 1.0)."""
257
+
258
+
259
+ class AgentBudgetStatus(TypedDict):
260
+ """Live utilization from ``GET /v1/agents/me/budgets``. Mirrors ``AgentBudgetStatus`` in TS."""
261
+
262
+ daily_usdc: Optional[float]
263
+ per_transaction_usdc: Optional[float]
264
+ alert_at: Optional[float]
265
+ pause_at: Optional[float]
266
+ spent_today_usdc: str
267
+ remaining_usdc: Optional[str]
268
+ pct_used: Optional[float]
269
+ resets_at: str