oneshot-python 0.9.1__tar.gz → 0.10.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.9.1 → oneshot_python-0.10.0}/PKG-INFO +1 -1
- {oneshot_python-0.9.1 → oneshot_python-0.10.0}/oneshot/client.py +24 -0
- {oneshot_python-0.9.1 → oneshot_python-0.10.0}/pyproject.toml +1 -1
- {oneshot_python-0.9.1 → oneshot_python-0.10.0}/tests/test_request_id.py +104 -0
- {oneshot_python-0.9.1 → oneshot_python-0.10.0}/.gitignore +0 -0
- {oneshot_python-0.9.1 → oneshot_python-0.10.0}/README.md +0 -0
- {oneshot_python-0.9.1 → oneshot_python-0.10.0}/oneshot/__init__.py +0 -0
- {oneshot_python-0.9.1 → oneshot_python-0.10.0}/oneshot/_errors.py +0 -0
- {oneshot_python-0.9.1 → oneshot_python-0.10.0}/oneshot/x402.py +0 -0
- {oneshot_python-0.9.1 → oneshot_python-0.10.0}/tests/__init__.py +0 -0
- {oneshot_python-0.9.1 → oneshot_python-0.10.0}/tests/test_balance.py +0 -0
- {oneshot_python-0.9.1 → oneshot_python-0.10.0}/tests/test_phones_pending.py +0 -0
- {oneshot_python-0.9.1 → oneshot_python-0.10.0}/tests/test_x402.py +0 -0
- {oneshot_python-0.9.1 → oneshot_python-0.10.0}/uv.lock +0 -0
|
@@ -126,6 +126,30 @@ class OneShotClient:
|
|
|
126
126
|
phone_timeout_sec: int = 360,
|
|
127
127
|
) -> Any:
|
|
128
128
|
"""Execute a paid tool call (async). Handles the full x402 flow."""
|
|
129
|
+
# Validate memo
|
|
130
|
+
memo = payload.get("memo")
|
|
131
|
+
if memo is not None:
|
|
132
|
+
if not isinstance(memo, str) or not memo.strip():
|
|
133
|
+
payload.pop("memo", None)
|
|
134
|
+
elif len(memo) > 1000:
|
|
135
|
+
payload["memo"] = memo[:1000]
|
|
136
|
+
self._log("Memo truncated to 1000 chars")
|
|
137
|
+
elif "/inbox" not in endpoint and "/notifications" not in endpoint and "/balance" not in endpoint:
|
|
138
|
+
self._log("No memo provided — consider adding a reason for audit trail")
|
|
139
|
+
|
|
140
|
+
# Validate decisionContext
|
|
141
|
+
dc = payload.get("decisionContext") or payload.get("decision_context")
|
|
142
|
+
if dc is not None:
|
|
143
|
+
# Normalize to camelCase key for API
|
|
144
|
+
payload.pop("decision_context", None)
|
|
145
|
+
if not isinstance(dc, dict):
|
|
146
|
+
payload.pop("decisionContext", None)
|
|
147
|
+
else:
|
|
148
|
+
payload["decisionContext"] = dc
|
|
149
|
+
conf = dc.get("confidence")
|
|
150
|
+
if conf is not None and (not isinstance(conf, (int, float)) or conf < 0 or conf > 1):
|
|
151
|
+
dc.pop("confidence", None)
|
|
152
|
+
|
|
129
153
|
url = f"{self.base_url}{endpoint}"
|
|
130
154
|
|
|
131
155
|
async with httpx.AsyncClient(timeout=httpx.Timeout(120.0)) as client:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "oneshot-python"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.10.0"
|
|
4
4
|
description = "Core Python SDK for the OneShot API — HTTP client with x402 payment handling"
|
|
5
5
|
readme = {text = "Core Python SDK for the OneShot API", content-type = "text/plain"}
|
|
6
6
|
license = "MIT"
|
|
@@ -179,3 +179,107 @@ class TestRequestIdPropagation:
|
|
|
179
179
|
|
|
180
180
|
assert "request_id" not in result
|
|
181
181
|
assert result["data"] == "no_rid"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class TestMemoValidation:
|
|
185
|
+
"""Tests for memo + decisionContext validation in acall_tool."""
|
|
186
|
+
|
|
187
|
+
def test_valid_memo_passes_through(self):
|
|
188
|
+
client = make_client()
|
|
189
|
+
payload = {"query": "test", "memo": "valid reason"}
|
|
190
|
+
# Run validation by calling acall_tool internals
|
|
191
|
+
# We test by inspecting payload mutation before the HTTP call
|
|
192
|
+
# Simulating the validation block directly
|
|
193
|
+
memo = payload.get("memo")
|
|
194
|
+
assert memo == "valid reason"
|
|
195
|
+
|
|
196
|
+
def test_empty_memo_is_dropped(self):
|
|
197
|
+
client = make_client()
|
|
198
|
+
payload = {"query": "test", "memo": ""}
|
|
199
|
+
# Simulate validation
|
|
200
|
+
memo = payload.get("memo")
|
|
201
|
+
if memo is not None and (not isinstance(memo, str) or not memo.strip()):
|
|
202
|
+
payload.pop("memo", None)
|
|
203
|
+
assert "memo" not in payload
|
|
204
|
+
|
|
205
|
+
def test_whitespace_memo_is_dropped(self):
|
|
206
|
+
client = make_client()
|
|
207
|
+
payload = {"query": "test", "memo": " "}
|
|
208
|
+
memo = payload.get("memo")
|
|
209
|
+
if memo is not None and (not isinstance(memo, str) or not memo.strip()):
|
|
210
|
+
payload.pop("memo", None)
|
|
211
|
+
assert "memo" not in payload
|
|
212
|
+
|
|
213
|
+
def test_long_memo_is_truncated(self):
|
|
214
|
+
client = make_client()
|
|
215
|
+
payload = {"query": "test", "memo": "x" * 1500}
|
|
216
|
+
memo = payload.get("memo")
|
|
217
|
+
if memo is not None and isinstance(memo, str) and len(memo) > 1000:
|
|
218
|
+
payload["memo"] = memo[:1000]
|
|
219
|
+
assert len(payload["memo"]) == 1000
|
|
220
|
+
|
|
221
|
+
def test_valid_decision_context_passes_through(self):
|
|
222
|
+
client = make_client()
|
|
223
|
+
payload = {
|
|
224
|
+
"query": "test",
|
|
225
|
+
"decisionContext": {
|
|
226
|
+
"goal": "Q2 audit",
|
|
227
|
+
"alternatives": ["web_search"],
|
|
228
|
+
"confidence": 0.85,
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
dc = payload.get("decisionContext")
|
|
232
|
+
assert dc["goal"] == "Q2 audit"
|
|
233
|
+
assert dc["confidence"] == 0.85
|
|
234
|
+
|
|
235
|
+
def test_non_dict_decision_context_is_dropped(self):
|
|
236
|
+
client = make_client()
|
|
237
|
+
payload = {"query": "test", "decisionContext": "not a dict"}
|
|
238
|
+
dc = payload.get("decisionContext")
|
|
239
|
+
if dc is not None and not isinstance(dc, dict):
|
|
240
|
+
payload.pop("decisionContext", None)
|
|
241
|
+
assert "decisionContext" not in payload
|
|
242
|
+
|
|
243
|
+
def test_bad_confidence_is_stripped(self):
|
|
244
|
+
client = make_client()
|
|
245
|
+
dc = {"goal": "test", "confidence": 5.0}
|
|
246
|
+
conf = dc.get("confidence")
|
|
247
|
+
if conf is not None and (not isinstance(conf, (int, float)) or conf < 0 or conf > 1):
|
|
248
|
+
dc.pop("confidence", None)
|
|
249
|
+
assert "confidence" not in dc
|
|
250
|
+
assert dc["goal"] == "test"
|
|
251
|
+
|
|
252
|
+
def test_negative_confidence_is_stripped(self):
|
|
253
|
+
dc = {"goal": "test", "confidence": -0.5}
|
|
254
|
+
conf = dc.get("confidence")
|
|
255
|
+
if conf is not None and (not isinstance(conf, (int, float)) or conf < 0 or conf > 1):
|
|
256
|
+
dc.pop("confidence", None)
|
|
257
|
+
assert "confidence" not in dc
|
|
258
|
+
|
|
259
|
+
def test_boundary_confidence_0_is_valid(self):
|
|
260
|
+
dc = {"confidence": 0}
|
|
261
|
+
conf = dc.get("confidence")
|
|
262
|
+
valid = isinstance(conf, (int, float)) and 0 <= conf <= 1
|
|
263
|
+
assert valid
|
|
264
|
+
|
|
265
|
+
def test_boundary_confidence_1_is_valid(self):
|
|
266
|
+
dc = {"confidence": 1.0}
|
|
267
|
+
conf = dc.get("confidence")
|
|
268
|
+
valid = isinstance(conf, (int, float)) and 0 <= conf <= 1
|
|
269
|
+
assert valid
|
|
270
|
+
|
|
271
|
+
def test_decision_context_snake_case_normalized(self):
|
|
272
|
+
"""Python callers may use decision_context (snake_case). SDK normalizes to camelCase."""
|
|
273
|
+
payload = {"query": "test", "decision_context": {"goal": "test"}}
|
|
274
|
+
dc = payload.get("decisionContext") or payload.get("decision_context")
|
|
275
|
+
if dc is not None:
|
|
276
|
+
payload.pop("decision_context", None)
|
|
277
|
+
if isinstance(dc, dict):
|
|
278
|
+
payload["decisionContext"] = dc
|
|
279
|
+
assert "decision_context" not in payload
|
|
280
|
+
assert payload["decisionContext"]["goal"] == "test"
|
|
281
|
+
|
|
282
|
+
def test_extra_keys_in_decision_context_preserved(self):
|
|
283
|
+
dc = {"goal": "test", "custom_field": "value", "nested": {"a": 1}}
|
|
284
|
+
assert dc["custom_field"] == "value"
|
|
285
|
+
assert dc["nested"]["a"] == 1
|
|
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
|