Skip to content

Commit b481258

Browse files
feat: sync SDK with API changes
1 parent 599b5f8 commit b481258

7 files changed

Lines changed: 289 additions & 3 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "blindpay"
3-
version = "1.3.0"
3+
version = "1.4.0"
44
description = "Official Python SDK for the Blindpay API — Global payments infrastructure"
55
readme = "README.md"
66
authors = [{ name = "Blindpay", email = "[email protected]" }]

src/blindpay/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "1.3.0"
1+
__version__ = "1.4.0"
22

33
from ._internal.exceptions import BlindPayError
44
from .client import BlindPay, BlindPaySync

src/blindpay/client.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from blindpay.resources.payouts.payouts import PayoutsResource, PayoutsResourceSync
2626
from blindpay.resources.quotes.quotes import QuotesResource, QuotesResourceSync
2727
from blindpay.resources.receivers.receivers import ReceiversResource, ReceiversResourceSync
28+
from blindpay.resources.sandbox.sandbox import SandboxResource, SandboxResourceSync
2829
from blindpay.resources.terms_of_service.terms_of_service import (
2930
TermsOfServiceResource,
3031
TermsOfServiceResourceSync,
@@ -38,7 +39,7 @@
3839
from blindpay.resources.wallets.offramp import OfframpWalletsResource, OfframpWalletsResourceSync
3940
from blindpay.resources.webhooks.webhooks import WebhookEndpointsResource, WebhookEndpointsResourceSync
4041

41-
__version__ = "1.3.0"
42+
__version__ = "1.4.0"
4243

4344
T = TypeVar("T")
4445

@@ -308,6 +309,12 @@ def payouts(self) -> "PayoutsResource":
308309
def receivers(self) -> _ReceiversNamespace:
309310
return _ReceiversNamespace(self._instance_id, self._api)
310311

312+
@cached_property
313+
def sandbox(self) -> "SandboxResource":
314+
from blindpay.resources.sandbox import create_sandbox_resource
315+
316+
return create_sandbox_resource(self._instance_id, self._api)
317+
311318
@cached_property
312319
def virtual_accounts(self) -> "VirtualAccountsResource":
313320
from blindpay.resources.virtual_accounts import create_virtual_accounts_resource
@@ -526,6 +533,12 @@ def payouts(self) -> "PayoutsResourceSync":
526533
def receivers(self) -> _ReceiversNamespaceSync:
527534
return _ReceiversNamespaceSync(self._instance_id, self._api)
528535

536+
@cached_property
537+
def sandbox(self) -> "SandboxResourceSync":
538+
from blindpay.resources.sandbox import create_sandbox_resource_sync
539+
540+
return create_sandbox_resource_sync(self._instance_id, self._api)
541+
529542
@cached_property
530543
def virtual_accounts(self) -> "VirtualAccountsResourceSync":
531544
from blindpay.resources.virtual_accounts import create_virtual_accounts_resource_sync

src/blindpay/resources/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .payouts import create_payouts_resource
1010
from .quotes import create_quotes_resource
1111
from .receivers import create_receivers_resource
12+
from .sandbox import create_sandbox_resource, create_sandbox_resource_sync
1213
from .terms_of_service import create_terms_of_service_resource
1314
from .transfers import create_transfers_resource
1415
from .virtual_accounts import create_virtual_accounts_resource
@@ -28,6 +29,8 @@
2829
"create_payouts_resource",
2930
"create_quotes_resource",
3031
"create_receivers_resource",
32+
"create_sandbox_resource",
33+
"create_sandbox_resource_sync",
3134
"create_terms_of_service_resource",
3235
"create_transfers_resource",
3336
"create_virtual_accounts_resource",
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from .sandbox import (
2+
CreateSandboxInput,
3+
CreateSandboxResponse,
4+
ListSandboxResponse,
5+
SandboxItem,
6+
SandboxResource,
7+
SandboxResourceSync,
8+
SandboxStatus,
9+
UpdateSandboxInput,
10+
create_sandbox_resource,
11+
create_sandbox_resource_sync,
12+
)
13+
14+
__all__ = [
15+
"create_sandbox_resource",
16+
"create_sandbox_resource_sync",
17+
"SandboxResource",
18+
"SandboxResourceSync",
19+
"SandboxItem",
20+
"SandboxStatus",
21+
"CreateSandboxInput",
22+
"CreateSandboxResponse",
23+
"ListSandboxResponse",
24+
"UpdateSandboxInput",
25+
]
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from typing import Dict, List, Literal, Optional
2+
3+
from typing_extensions import TypedDict
4+
5+
from ..._internal.api_client import InternalApiClient, InternalApiClientSync
6+
from ...types import BlindpayApiResponse
7+
8+
SandboxStatus = Literal["active", "inactive"]
9+
10+
11+
class SandboxItem(TypedDict):
12+
id: str
13+
name: str
14+
status: SandboxStatus
15+
metadata: Optional[Dict[str, str]]
16+
created_at: Optional[str]
17+
updated_at: Optional[str]
18+
19+
20+
class CreateSandboxInput(TypedDict, total=False):
21+
name: str
22+
metadata: Dict[str, str]
23+
24+
25+
class CreateSandboxResponse(TypedDict):
26+
id: str
27+
name: str
28+
status: SandboxStatus
29+
metadata: Optional[Dict[str, str]]
30+
31+
32+
class ListSandboxResponse(TypedDict):
33+
data: List[SandboxItem]
34+
35+
36+
class _UpdateSandboxInputRequired(TypedDict):
37+
sandbox_id: str
38+
39+
40+
class UpdateSandboxInput(_UpdateSandboxInputRequired, total=False):
41+
name: str
42+
metadata: Dict[str, str]
43+
44+
45+
# --- Async resource ---
46+
47+
48+
class SandboxResource:
49+
def __init__(self, instance_id: str, client: InternalApiClient):
50+
self._instance_id = instance_id
51+
self._client = client
52+
53+
async def list(self) -> BlindpayApiResponse[List[SandboxItem]]:
54+
return await self._client.get(f"/instances/{self._instance_id}/sandbox")
55+
56+
async def get(self, sandbox_id: str) -> BlindpayApiResponse[SandboxItem]:
57+
return await self._client.get(f"/instances/{self._instance_id}/sandbox/{sandbox_id}")
58+
59+
async def create(self, data: CreateSandboxInput) -> BlindpayApiResponse[CreateSandboxResponse]:
60+
return await self._client.post(f"/instances/{self._instance_id}/sandbox", data)
61+
62+
async def update(self, data: UpdateSandboxInput) -> BlindpayApiResponse[SandboxItem]:
63+
sandbox_id = data["sandbox_id"]
64+
payload = {k: v for k, v in data.items() if k != "sandbox_id"}
65+
return await self._client.patch(f"/instances/{self._instance_id}/sandbox/{sandbox_id}", payload)
66+
67+
async def delete(self, sandbox_id: str) -> BlindpayApiResponse[None]:
68+
return await self._client.delete(f"/instances/{self._instance_id}/sandbox/{sandbox_id}")
69+
70+
71+
# --- Sync resource ---
72+
73+
74+
class SandboxResourceSync:
75+
def __init__(self, instance_id: str, client: InternalApiClientSync):
76+
self._instance_id = instance_id
77+
self._client = client
78+
79+
def list(self) -> BlindpayApiResponse[List[SandboxItem]]:
80+
return self._client.get(f"/instances/{self._instance_id}/sandbox")
81+
82+
def get(self, sandbox_id: str) -> BlindpayApiResponse[SandboxItem]:
83+
return self._client.get(f"/instances/{self._instance_id}/sandbox/{sandbox_id}")
84+
85+
def create(self, data: CreateSandboxInput) -> BlindpayApiResponse[CreateSandboxResponse]:
86+
return self._client.post(f"/instances/{self._instance_id}/sandbox", data)
87+
88+
def update(self, data: UpdateSandboxInput) -> BlindpayApiResponse[SandboxItem]:
89+
sandbox_id = data["sandbox_id"]
90+
payload = {k: v for k, v in data.items() if k != "sandbox_id"}
91+
return self._client.patch(f"/instances/{self._instance_id}/sandbox/{sandbox_id}", payload)
92+
93+
def delete(self, sandbox_id: str) -> BlindpayApiResponse[None]:
94+
return self._client.delete(f"/instances/{self._instance_id}/sandbox/{sandbox_id}")
95+
96+
97+
# --- Factory functions ---
98+
99+
100+
def create_sandbox_resource(instance_id: str, client: InternalApiClient) -> SandboxResource:
101+
return SandboxResource(instance_id, client)
102+
103+
104+
def create_sandbox_resource_sync(instance_id: str, client: InternalApiClientSync) -> SandboxResourceSync:
105+
return SandboxResourceSync(instance_id, client)

tests/resources/test_sandbox.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
from unittest.mock import patch
2+
3+
import pytest
4+
5+
from blindpay import BlindPay, BlindPaySync
6+
7+
8+
class TestSandbox:
9+
@pytest.fixture(autouse=True)
10+
def setup(self):
11+
self.blindpay = BlindPay(api_key="test-key", instance_id="in_000000000000")
12+
13+
@pytest.mark.asyncio
14+
async def test_list(self):
15+
mocked_data = [{"id": "sb_000000000000", "name": "My Sandbox Item", "status": "active"}]
16+
17+
with patch.object(self.blindpay._api, "_request") as mock_request:
18+
mock_request.return_value = {"data": mocked_data, "error": None}
19+
20+
response = await self.blindpay.sandbox.list()
21+
22+
assert response["error"] is None
23+
assert response["data"] == mocked_data
24+
mock_request.assert_called_once_with("GET", "/instances/in_000000000000/sandbox")
25+
26+
@pytest.mark.asyncio
27+
async def test_get(self):
28+
mocked_data = {"id": "sb_000000000000", "name": "My Sandbox Item", "status": "active"}
29+
30+
with patch.object(self.blindpay._api, "_request") as mock_request:
31+
mock_request.return_value = {"data": mocked_data, "error": None}
32+
33+
response = await self.blindpay.sandbox.get("sb_000000000000")
34+
35+
assert response["error"] is None
36+
assert response["data"] == mocked_data
37+
mock_request.assert_called_once_with("GET", "/instances/in_000000000000/sandbox/sb_000000000000")
38+
39+
@pytest.mark.asyncio
40+
async def test_create(self):
41+
mocked_data = {"id": "sb_000000000000", "name": "My Sandbox Item", "status": "active"}
42+
43+
with patch.object(self.blindpay._api, "_request") as mock_request:
44+
mock_request.return_value = {"data": mocked_data, "error": None}
45+
46+
response = await self.blindpay.sandbox.create({"name": "My Sandbox Item"})
47+
48+
assert response["error"] is None
49+
assert response["data"] == mocked_data
50+
mock_request.assert_called_once_with("POST", "/instances/in_000000000000/sandbox", {"name": "My Sandbox Item"})
51+
52+
@pytest.mark.asyncio
53+
async def test_update(self):
54+
mocked_data = {"id": "sb_000000000000", "name": "Updated Name", "status": "active"}
55+
56+
with patch.object(self.blindpay._api, "_request") as mock_request:
57+
mock_request.return_value = {"data": mocked_data, "error": None}
58+
59+
response = await self.blindpay.sandbox.update({"sandbox_id": "sb_000000000000", "name": "Updated Name"})
60+
61+
assert response["error"] is None
62+
assert response["data"] == mocked_data
63+
mock_request.assert_called_once_with(
64+
"PATCH", "/instances/in_000000000000/sandbox/sb_000000000000", {"name": "Updated Name"}
65+
)
66+
67+
@pytest.mark.asyncio
68+
async def test_delete(self):
69+
with patch.object(self.blindpay._api, "_request") as mock_request:
70+
mock_request.return_value = {"data": None, "error": None}
71+
72+
response = await self.blindpay.sandbox.delete("sb_000000000000")
73+
74+
assert response["error"] is None
75+
mock_request.assert_called_once_with("DELETE", "/instances/in_000000000000/sandbox/sb_000000000000", None)
76+
77+
78+
class TestSandboxSync:
79+
@pytest.fixture(autouse=True)
80+
def setup(self):
81+
self.blindpay = BlindPaySync(api_key="test-key", instance_id="in_000000000000")
82+
83+
def test_list(self):
84+
mocked_data = [{"id": "sb_000000000000", "name": "My Sandbox Item", "status": "active"}]
85+
86+
with patch.object(self.blindpay._api, "_request") as mock_request:
87+
mock_request.return_value = {"data": mocked_data, "error": None}
88+
89+
response = self.blindpay.sandbox.list()
90+
91+
assert response["error"] is None
92+
assert response["data"] == mocked_data
93+
mock_request.assert_called_once_with("GET", "/instances/in_000000000000/sandbox")
94+
95+
def test_get(self):
96+
mocked_data = {"id": "sb_000000000000", "name": "My Sandbox Item", "status": "active"}
97+
98+
with patch.object(self.blindpay._api, "_request") as mock_request:
99+
mock_request.return_value = {"data": mocked_data, "error": None}
100+
101+
response = self.blindpay.sandbox.get("sb_000000000000")
102+
103+
assert response["error"] is None
104+
assert response["data"] == mocked_data
105+
mock_request.assert_called_once_with("GET", "/instances/in_000000000000/sandbox/sb_000000000000")
106+
107+
def test_create(self):
108+
mocked_data = {"id": "sb_000000000000", "name": "My Sandbox Item", "status": "active"}
109+
110+
with patch.object(self.blindpay._api, "_request") as mock_request:
111+
mock_request.return_value = {"data": mocked_data, "error": None}
112+
113+
response = self.blindpay.sandbox.create({"name": "My Sandbox Item"})
114+
115+
assert response["error"] is None
116+
assert response["data"] == mocked_data
117+
mock_request.assert_called_once_with("POST", "/instances/in_000000000000/sandbox", {"name": "My Sandbox Item"})
118+
119+
def test_update(self):
120+
mocked_data = {"id": "sb_000000000000", "name": "Updated Name", "status": "active"}
121+
122+
with patch.object(self.blindpay._api, "_request") as mock_request:
123+
mock_request.return_value = {"data": mocked_data, "error": None}
124+
125+
response = self.blindpay.sandbox.update({"sandbox_id": "sb_000000000000", "name": "Updated Name"})
126+
127+
assert response["error"] is None
128+
assert response["data"] == mocked_data
129+
mock_request.assert_called_once_with(
130+
"PATCH", "/instances/in_000000000000/sandbox/sb_000000000000", {"name": "Updated Name"}
131+
)
132+
133+
def test_delete(self):
134+
with patch.object(self.blindpay._api, "_request") as mock_request:
135+
mock_request.return_value = {"data": None, "error": None}
136+
137+
response = self.blindpay.sandbox.delete("sb_000000000000")
138+
139+
assert response["error"] is None
140+
mock_request.assert_called_once_with("DELETE", "/instances/in_000000000000/sandbox/sb_000000000000", None)

0 commit comments

Comments
 (0)