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.
- {oneshot_python-0.18.0 → oneshot_python-0.20.2}/.gitignore +3 -0
- {oneshot_python-0.18.0 → oneshot_python-0.20.2}/PKG-INFO +1 -1
- {oneshot_python-0.18.0 → oneshot_python-0.20.2}/README.md +37 -0
- {oneshot_python-0.18.0 → oneshot_python-0.20.2}/oneshot/__init__.py +10 -0
- oneshot_python-0.20.2/oneshot/_errors.py +129 -0
- {oneshot_python-0.18.0 → oneshot_python-0.20.2}/oneshot/_types.py +40 -0
- {oneshot_python-0.18.0 → oneshot_python-0.20.2}/oneshot/client.py +382 -21
- {oneshot_python-0.18.0 → oneshot_python-0.20.2}/pyproject.toml +1 -1
- oneshot_python-0.20.2/tests/test_budgets.py +184 -0
- oneshot_python-0.20.2/tests/test_charge_amount.py +57 -0
- oneshot_python-0.20.2/tests/test_payment_rejection.py +88 -0
- oneshot_python-0.20.2/tests/test_poll_backoff.py +107 -0
- {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/test_request_id.py +1 -4
- oneshot_python-0.20.2/tests/test_wait_false.py +88 -0
- oneshot_python-0.18.0/oneshot/_errors.py +0 -56
- {oneshot_python-0.18.0 → oneshot_python-0.20.2}/oneshot/x402.py +0 -0
- {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/__init__.py +0 -0
- {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/test_balance.py +0 -0
- {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/test_compute.py +0 -0
- {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/test_domains.py +0 -0
- {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/test_email_payload.py +0 -0
- {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/test_emergency_error.py +0 -0
- {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/test_max_cost_header.py +0 -0
- {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/test_phones_pending.py +0 -0
- {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/test_tag_receipt_value.py +0 -0
- {oneshot_python-0.18.0 → oneshot_python-0.20.2}/tests/test_x402.py +0 -0
- {oneshot_python-0.18.0 → oneshot_python-0.20.2}/uv.lock +0 -0
|
@@ -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
|