oneshot-python 0.6.0__tar.gz → 0.7.1__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.
@@ -71,5 +71,8 @@ tmp/
71
71
  # Secrets
72
72
  soul-api-keys.json
73
73
 
74
+ # Next.js
75
+ .next/
76
+
74
77
  # Build outputs
75
78
  apps/video-service/out/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oneshot-python
3
- Version: 0.6.0
3
+ Version: 0.7.1
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
@@ -137,9 +137,10 @@ class OneShotClient:
137
137
 
138
138
  # Step 2 — Parse 402 response
139
139
  # Parse x402 v2 PaymentRequired from PAYMENT-REQUIRED header
140
- accepted = parse_payment_required(
140
+ parsed_req = parse_payment_required(
141
141
  resp.headers.get("payment-required")
142
142
  )
143
+ accepted = parsed_req["accepted"]
143
144
 
144
145
  quote_data = resp.json()
145
146
  payment_request = quote_data["payment_request"]
@@ -156,7 +157,7 @@ class OneShotClient:
156
157
 
157
158
  self._log(f"Payment required: {payment_request['amount']} USDC")
158
159
 
159
- # Step 3 — Sign x402 payment (v2 format with accepted requirements)
160
+ # Step 3 — Sign x402 payment (v2 format with accepted requirements + Bazaar metadata)
160
161
  auth = sign_payment_authorization(
161
162
  private_key=self._private_key,
162
163
  from_address=self.address,
@@ -166,6 +167,8 @@ class OneShotClient:
166
167
  chain_id=payment_request["chain_id"],
167
168
  network=f"eip155:{payment_request['chain_id']}",
168
169
  accepted=accepted,
170
+ resource=parsed_req.get("resource"),
171
+ extensions=parsed_req.get("extensions"),
169
172
  )
170
173
 
171
174
  # Step 4 — Re-POST with payment headers (x402 format)
@@ -275,6 +278,18 @@ class OneShotClient:
275
278
  return {"success": True}
276
279
  return resp.json()
277
280
 
281
+ # ------------------------------------------------------------------
282
+ # Balance
283
+ # ------------------------------------------------------------------
284
+
285
+ def get_balance(self) -> dict:
286
+ """Get unified balance (on-chain USDC + credits). Blocking."""
287
+ return self.call_free_get("/v1/tools/balance")
288
+
289
+ async def aget_balance(self) -> dict:
290
+ """Get unified balance (on-chain USDC + credits). Async."""
291
+ return await self.acall_free_get("/v1/tools/balance")
292
+
278
293
  # ------------------------------------------------------------------
279
294
  # Job polling
280
295
  # ------------------------------------------------------------------
@@ -39,8 +39,16 @@ class PaymentAuthorization(TypedDict):
39
39
  payload: dict[str, Any]
40
40
 
41
41
 
42
- def parse_payment_required(header: str | None) -> PaymentRequirements:
43
- """Decode the base64 PAYMENT-REQUIRED header and return the first accepted option.
42
+ class ParsedPaymentRequired(TypedDict, total=False):
43
+ """Full parsed 402 response with Bazaar discovery metadata."""
44
+
45
+ accepted: PaymentRequirements
46
+ resource: dict[str, Any]
47
+ extensions: dict[str, Any]
48
+
49
+
50
+ def parse_payment_required(header: str | None) -> ParsedPaymentRequired:
51
+ """Decode the base64 PAYMENT-REQUIRED header and return accepted requirements + Bazaar metadata.
44
52
 
45
53
  Falls back to sensible defaults if the header is missing or unparseable.
46
54
  """
@@ -48,22 +56,29 @@ def parse_payment_required(header: str | None) -> PaymentRequirements:
48
56
  try:
49
57
  decoded = base64.b64decode(header).decode()
50
58
  parsed = json.loads(decoded)
51
- # x402 v2: { x402Version: 2, accepts: [...], resource: {...} }
59
+ # x402 v2: { x402Version: 2, accepts: [...], resource: {...}, extensions: {...} }
52
60
  accepts = parsed.get("accepts", [])
53
61
  if accepts:
54
- return accepts[0]
62
+ result: ParsedPaymentRequired = {"accepted": accepts[0]}
63
+ if parsed.get("resource"):
64
+ result["resource"] = parsed["resource"]
65
+ if parsed.get("extensions"):
66
+ result["extensions"] = parsed["extensions"]
67
+ return result
55
68
  except Exception:
56
69
  pass
57
70
 
58
71
  # Fallback with production defaults
59
- return PaymentRequirements(
60
- scheme="exact",
61
- network="eip155:8453",
62
- amount="0",
63
- asset="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
64
- payTo="",
65
- maxTimeoutSeconds=300,
66
- extra={"name": "USD Coin", "version": "2"},
72
+ return ParsedPaymentRequired(
73
+ accepted=PaymentRequirements(
74
+ scheme="exact",
75
+ network="eip155:8453",
76
+ amount="0",
77
+ asset="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
78
+ payTo="",
79
+ maxTimeoutSeconds=300,
80
+ extra={"name": "USD Coin", "version": "2"},
81
+ ),
67
82
  )
68
83
 
69
84
 
@@ -77,6 +92,8 @@ def sign_payment_authorization(
77
92
  chain_id: int,
78
93
  network: str,
79
94
  accepted: PaymentRequirements | None = None,
95
+ resource: dict[str, Any] | None = None,
96
+ extensions: dict[str, Any] | None = None,
80
97
  ) -> dict[str, Any]:
81
98
  """Create an EIP-712 TransferWithAuthorization signature for USDC.
82
99
 
@@ -89,6 +106,8 @@ def sign_payment_authorization(
89
106
  chain_id: EVM chain ID (8453 for Base Mainnet).
90
107
  network: CAIP-2 network string (e.g. "eip155:8453").
91
108
  accepted: Parsed PaymentRequirements from server's PAYMENT-REQUIRED header.
109
+ resource: Resource metadata from 402 response (for Bazaar discovery).
110
+ extensions: Extensions from 402 response (for Bazaar discovery).
92
111
 
93
112
  Returns:
94
113
  x402 PaymentPayload v2 dict.
@@ -150,8 +169,8 @@ def sign_payment_authorization(
150
169
  extra={"name": domain_name, "version": domain_version},
151
170
  )
152
171
 
153
- # x402 PaymentPayload v2 format
154
- return {
172
+ # x402 PaymentPayload v2 format (including resource + extensions for Bazaar discovery)
173
+ result: dict[str, Any] = {
155
174
  "x402Version": 2,
156
175
  "accepted": dict(accepted_req),
157
176
  "payload": {
@@ -166,6 +185,11 @@ def sign_payment_authorization(
166
185
  },
167
186
  },
168
187
  }
188
+ if resource:
189
+ result["resource"] = resource
190
+ if extensions:
191
+ result["extensions"] = extensions
192
+ return result
169
193
 
170
194
 
171
195
  def encode_payment_header(auth: dict[str, Any]) -> str:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "oneshot-python"
3
- version = "0.6.0"
3
+ version = "0.7.1"
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"
@@ -0,0 +1,98 @@
1
+ """Tests for OneShotClient.get_balance() and aget_balance().
2
+
3
+ Uses mock HTTP responses to verify the client correctly calls the
4
+ /v1/tools/balance endpoint and returns the parsed response.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from unittest.mock import AsyncMock, MagicMock, patch
11
+
12
+ import pytest
13
+ from eth_account import Account
14
+
15
+ from oneshot.client import OneShotClient
16
+
17
+ # Deterministic test key (never use with real funds)
18
+ TEST_PRIVATE_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
19
+ TEST_ADDRESS = Account.from_key(TEST_PRIVATE_KEY).address
20
+
21
+ MOCK_BALANCE_RESPONSE = {
22
+ "on_chain_balance": "125.500000",
23
+ "credits_balance": "45.00",
24
+ "currency": "USDC",
25
+ "address": TEST_ADDRESS,
26
+ "chain_id": 8453,
27
+ }
28
+
29
+
30
+ class TestGetBalance:
31
+ """Synchronous get_balance() tests."""
32
+
33
+ def test_calls_correct_endpoint(self) -> None:
34
+ client = OneShotClient(TEST_PRIVATE_KEY)
35
+
36
+ with patch.object(client, "call_free_get", return_value=MOCK_BALANCE_RESPONSE) as mock:
37
+ result = client.get_balance()
38
+
39
+ mock.assert_called_once_with("/v1/tools/balance")
40
+ assert result == MOCK_BALANCE_RESPONSE
41
+
42
+ def test_returns_all_fields(self) -> None:
43
+ client = OneShotClient(TEST_PRIVATE_KEY)
44
+
45
+ with patch.object(client, "call_free_get", return_value=MOCK_BALANCE_RESPONSE):
46
+ result = client.get_balance()
47
+
48
+ assert result["on_chain_balance"] == "125.500000"
49
+ assert result["credits_balance"] == "45.00"
50
+ assert result["currency"] == "USDC"
51
+ assert result["address"] == TEST_ADDRESS
52
+ assert result["chain_id"] == 8453
53
+
54
+ def test_propagates_errors(self) -> None:
55
+ client = OneShotClient(TEST_PRIVATE_KEY)
56
+
57
+ with patch.object(client, "call_free_get", side_effect=Exception("Network error")):
58
+ with pytest.raises(Exception, match="Network error"):
59
+ client.get_balance()
60
+
61
+
62
+ class TestAgetBalance:
63
+ """Async aget_balance() tests."""
64
+
65
+ @pytest.mark.asyncio
66
+ async def test_calls_correct_endpoint(self) -> None:
67
+ client = OneShotClient(TEST_PRIVATE_KEY)
68
+
69
+ with patch.object(
70
+ client, "acall_free_get", new_callable=AsyncMock, return_value=MOCK_BALANCE_RESPONSE
71
+ ) as mock:
72
+ result = await client.aget_balance()
73
+
74
+ mock.assert_called_once_with("/v1/tools/balance")
75
+ assert result == MOCK_BALANCE_RESPONSE
76
+
77
+ @pytest.mark.asyncio
78
+ async def test_returns_all_fields(self) -> None:
79
+ client = OneShotClient(TEST_PRIVATE_KEY)
80
+
81
+ with patch.object(
82
+ client, "acall_free_get", new_callable=AsyncMock, return_value=MOCK_BALANCE_RESPONSE
83
+ ):
84
+ result = await client.aget_balance()
85
+
86
+ assert result["on_chain_balance"] == "125.500000"
87
+ assert result["credits_balance"] == "45.00"
88
+ assert result["currency"] == "USDC"
89
+
90
+ @pytest.mark.asyncio
91
+ async def test_propagates_errors(self) -> None:
92
+ client = OneShotClient(TEST_PRIVATE_KEY)
93
+
94
+ with patch.object(
95
+ client, "acall_free_get", new_callable=AsyncMock, side_effect=Exception("Timeout")
96
+ ):
97
+ with pytest.raises(Exception, match="Timeout"):
98
+ await client.aget_balance()
@@ -65,22 +65,25 @@ class TestParsePaymentRequired:
65
65
  header_data = {
66
66
  "x402Version": 2,
67
67
  "accepts": [MOCK_ACCEPTED],
68
- "resource": {},
68
+ "resource": {"url": "https://example.com/api"},
69
+ "extensions": {"bazaar": {"info": {}}},
69
70
  }
70
71
  encoded = base64.b64encode(json.dumps(header_data).encode()).decode()
71
72
  result = parse_payment_required(encoded)
72
- assert result["scheme"] == "exact"
73
- assert result["network"] == "eip155:8453"
74
- assert result["payTo"] == TO_ADDRESS
73
+ assert result["accepted"]["scheme"] == "exact"
74
+ assert result["accepted"]["network"] == "eip155:8453"
75
+ assert result["accepted"]["payTo"] == TO_ADDRESS
76
+ assert result["resource"]["url"] == "https://example.com/api"
77
+ assert "bazaar" in result["extensions"]
75
78
 
76
79
  def test_returns_fallback_on_none(self) -> None:
77
80
  result = parse_payment_required(None)
78
- assert result["scheme"] == "exact"
79
- assert result["extra"]["name"] == "USD Coin"
81
+ assert result["accepted"]["scheme"] == "exact"
82
+ assert result["accepted"]["extra"]["name"] == "USD Coin"
80
83
 
81
84
  def test_returns_fallback_on_invalid_base64(self) -> None:
82
85
  result = parse_payment_required("not-valid-base64!!!")
83
- assert result["scheme"] == "exact"
86
+ assert result["accepted"]["scheme"] == "exact"
84
87
 
85
88
 
86
89
  class TestSignPaymentAuthorization:
@@ -549,7 +549,7 @@ wheels = [
549
549
 
550
550
  [[package]]
551
551
  name = "oneshot-python"
552
- version = "0.5.0"
552
+ version = "0.7.0"
553
553
  source = { editable = "." }
554
554
  dependencies = [
555
555
  { name = "eth-account" },
File without changes