Skip to content

Commit 4ebe6dc

Browse files
feat: sync SDK with API changes
1 parent cc6f947 commit 4ebe6dc

7 files changed

Lines changed: 318 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

@@ -330,6 +331,12 @@ def fees(self) -> "FeesResource":
330331

331332
return create_fees_resource(self._instance_id, self._api)
332333

334+
@cached_property
335+
def sandbox(self) -> "SandboxResource":
336+
from blindpay.resources.sandbox import create_sandbox_resource
337+
338+
return create_sandbox_resource(self._instance_id, self._api)
339+
333340
def verify_webhook_signature(
334341
self, *, secret: str, id: str, timestamp: str, payload: str, svix_signature: str
335342
) -> bool:
@@ -548,6 +555,12 @@ def fees(self) -> "FeesResourceSync":
548555

549556
return create_fees_resource_sync(self._instance_id, self._api)
550557

558+
@cached_property
559+
def sandbox(self) -> "SandboxResourceSync":
560+
from blindpay.resources.sandbox import create_sandbox_resource_sync
561+
562+
return create_sandbox_resource_sync(self._instance_id, self._api)
563+
551564
def verify_webhook_signature(
552565
self, *, secret: str, id: str, timestamp: str, payload: str, svix_signature: str
553566
) -> bool:

src/blindpay/resources/__init__.py

Lines changed: 2 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
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,7 @@
2829
"create_payouts_resource",
2930
"create_quotes_resource",
3031
"create_receivers_resource",
32+
"create_sandbox_resource",
3133
"create_terms_of_service_resource",
3234
"create_transfers_resource",
3335
"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+
Sandbox,
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+
"Sandbox",
18+
"SandboxResource",
19+
"SandboxResourceSync",
20+
"SandboxStatus",
21+
"CreateSandboxInput",
22+
"CreateSandboxResponse",
23+
"ListSandboxResponse",
24+
"UpdateSandboxInput",
25+
]
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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 Sandbox(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 _CreateSandboxBase(TypedDict):
21+
name: str
22+
23+
24+
class CreateSandboxInput(_CreateSandboxBase, total=False):
25+
metadata: Dict[str, str]
26+
27+
28+
class CreateSandboxResponse(TypedDict):
29+
id: str
30+
name: str
31+
status: SandboxStatus
32+
metadata: Optional[Dict[str, str]]
33+
34+
35+
ListSandboxResponse = List[Sandbox]
36+
37+
38+
class _UpdateSandboxBase(TypedDict):
39+
sandbox_id: str
40+
41+
42+
class UpdateSandboxInput(_UpdateSandboxBase, total=False):
43+
name: str
44+
metadata: Dict[str, str]
45+
46+
47+
# --- Async resource ---
48+
49+
50+
class SandboxResource:
51+
def __init__(self, instance_id: str, client: InternalApiClient):
52+
self._instance_id = instance_id
53+
self._client = client
54+
55+
async def list(self) -> BlindpayApiResponse[ListSandboxResponse]:
56+
return await self._client.get(f"/instances/{self._instance_id}/sandbox")
57+
58+
async def get(self, sandbox_id: str) -> BlindpayApiResponse[Sandbox]:
59+
return await self._client.get(f"/instances/{self._instance_id}/sandbox/{sandbox_id}")
60+
61+
async def create(self, data: CreateSandboxInput) -> BlindpayApiResponse[CreateSandboxResponse]:
62+
return await self._client.post(f"/instances/{self._instance_id}/sandbox", data)
63+
64+
async def update(self, data: UpdateSandboxInput) -> BlindpayApiResponse[Sandbox]:
65+
sandbox_id = data["sandbox_id"]
66+
payload = {k: v for k, v in data.items() if k != "sandbox_id"}
67+
return await self._client.patch(f"/instances/{self._instance_id}/sandbox/{sandbox_id}", payload)
68+
69+
async def delete(self, sandbox_id: str) -> BlindpayApiResponse[None]:
70+
return await self._client.delete(f"/instances/{self._instance_id}/sandbox/{sandbox_id}")
71+
72+
73+
# --- Sync resource ---
74+
75+
76+
class SandboxResourceSync:
77+
def __init__(self, instance_id: str, client: InternalApiClientSync):
78+
self._instance_id = instance_id
79+
self._client = client
80+
81+
def list(self) -> BlindpayApiResponse[ListSandboxResponse]:
82+
return self._client.get(f"/instances/{self._instance_id}/sandbox")
83+
84+
def get(self, sandbox_id: str) -> BlindpayApiResponse[Sandbox]:
85+
return self._client.get(f"/instances/{self._instance_id}/sandbox/{sandbox_id}")
86+
87+
def create(self, data: CreateSandboxInput) -> BlindpayApiResponse[CreateSandboxResponse]:
88+
return self._client.post(f"/instances/{self._instance_id}/sandbox", data)
89+
90+
def update(self, data: UpdateSandboxInput) -> BlindpayApiResponse[Sandbox]:
91+
sandbox_id = data["sandbox_id"]
92+
payload = {k: v for k, v in data.items() if k != "sandbox_id"}
93+
return self._client.patch(f"/instances/{self._instance_id}/sandbox/{sandbox_id}", payload)
94+
95+
def delete(self, sandbox_id: str) -> BlindpayApiResponse[None]:
96+
return self._client.delete(f"/instances/{self._instance_id}/sandbox/{sandbox_id}")
97+
98+
99+
# --- Factory functions ---
100+
101+
102+
def create_sandbox_resource(instance_id: str, client: InternalApiClient) -> SandboxResource:
103+
return SandboxResource(instance_id, client)
104+
105+
106+
def create_sandbox_resource_sync(instance_id: str, client: InternalApiClientSync) -> SandboxResourceSync:
107+
return SandboxResourceSync(instance_id, client)

tests/resources/test_sandbox.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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", "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_sandbox = {
29+
"id": "sb_000000000000",
30+
"name": "My Sandbox",
31+
"status": "active",
32+
"metadata": None,
33+
"created_at": "2021-01-01T00:00:00Z",
34+
"updated_at": "2021-01-01T00:00:00Z",
35+
}
36+
37+
with patch.object(self.blindpay._api, "_request") as mock_request:
38+
mock_request.return_value = {"data": mocked_sandbox, "error": None}
39+
40+
response = await self.blindpay.sandbox.get("sb_000000000000")
41+
42+
assert response["error"] is None
43+
assert response["data"] == mocked_sandbox
44+
mock_request.assert_called_once_with("GET", "/instances/in_000000000000/sandbox/sb_000000000000")
45+
46+
@pytest.mark.asyncio
47+
async def test_create(self):
48+
mocked_response = {"id": "sb_000000000000", "name": "My Sandbox", "status": "active"}
49+
50+
with patch.object(self.blindpay._api, "_request") as mock_request:
51+
mock_request.return_value = {"data": mocked_response, "error": None}
52+
53+
response = await self.blindpay.sandbox.create({"name": "My Sandbox"})
54+
55+
assert response["error"] is None
56+
assert response["data"] == mocked_response
57+
mock_request.assert_called_once_with("POST", "/instances/in_000000000000/sandbox", {"name": "My Sandbox"})
58+
59+
@pytest.mark.asyncio
60+
async def test_update(self):
61+
mocked_sandbox = {
62+
"id": "sb_000000000000",
63+
"name": "Updated Sandbox",
64+
"status": "active",
65+
"metadata": None,
66+
"created_at": "2021-01-01T00:00:00Z",
67+
"updated_at": "2021-01-01T00:00:00Z",
68+
}
69+
70+
with patch.object(self.blindpay._api, "_request") as mock_request:
71+
mock_request.return_value = {"data": mocked_sandbox, "error": None}
72+
73+
response = await self.blindpay.sandbox.update({"sandbox_id": "sb_000000000000", "name": "Updated Sandbox"})
74+
75+
assert response["error"] is None
76+
assert response["data"] == mocked_sandbox
77+
mock_request.assert_called_once_with(
78+
"PATCH", "/instances/in_000000000000/sandbox/sb_000000000000", {"name": "Updated Sandbox"}
79+
)
80+
81+
@pytest.mark.asyncio
82+
async def test_delete(self):
83+
with patch.object(self.blindpay._api, "_request") as mock_request:
84+
mock_request.return_value = {"data": {"success": True}, "error": None}
85+
86+
response = await self.blindpay.sandbox.delete("sb_000000000000")
87+
88+
assert response["error"] is None
89+
mock_request.assert_called_once_with("DELETE", "/instances/in_000000000000/sandbox/sb_000000000000", None)
90+
91+
92+
class TestSandboxSync:
93+
@pytest.fixture(autouse=True)
94+
def setup(self):
95+
self.blindpay = BlindPaySync(api_key="test-key", instance_id="in_000000000000")
96+
97+
def test_list(self):
98+
mocked_data = [{"id": "sb_000000000000", "name": "My Sandbox", "status": "active"}]
99+
100+
with patch.object(self.blindpay._api, "_request") as mock_request:
101+
mock_request.return_value = {"data": mocked_data, "error": None}
102+
103+
response = self.blindpay.sandbox.list()
104+
105+
assert response["error"] is None
106+
assert response["data"] == mocked_data
107+
mock_request.assert_called_once_with("GET", "/instances/in_000000000000/sandbox")
108+
109+
def test_get(self):
110+
mocked_sandbox = {
111+
"id": "sb_000000000000",
112+
"name": "My Sandbox",
113+
"status": "active",
114+
"metadata": None,
115+
"created_at": "2021-01-01T00:00:00Z",
116+
"updated_at": "2021-01-01T00:00:00Z",
117+
}
118+
119+
with patch.object(self.blindpay._api, "_request") as mock_request:
120+
mock_request.return_value = {"data": mocked_sandbox, "error": None}
121+
122+
response = self.blindpay.sandbox.get("sb_000000000000")
123+
124+
assert response["error"] is None
125+
assert response["data"] == mocked_sandbox
126+
mock_request.assert_called_once_with("GET", "/instances/in_000000000000/sandbox/sb_000000000000")
127+
128+
def test_create(self):
129+
mocked_response = {"id": "sb_000000000000", "name": "My Sandbox", "status": "active"}
130+
131+
with patch.object(self.blindpay._api, "_request") as mock_request:
132+
mock_request.return_value = {"data": mocked_response, "error": None}
133+
134+
response = self.blindpay.sandbox.create({"name": "My Sandbox"})
135+
136+
assert response["error"] is None
137+
assert response["data"] == mocked_response
138+
mock_request.assert_called_once_with("POST", "/instances/in_000000000000/sandbox", {"name": "My Sandbox"})
139+
140+
def test_update(self):
141+
mocked_sandbox = {
142+
"id": "sb_000000000000",
143+
"name": "Updated Sandbox",
144+
"status": "active",
145+
"metadata": None,
146+
"created_at": "2021-01-01T00:00:00Z",
147+
"updated_at": "2021-01-01T00:00:00Z",
148+
}
149+
150+
with patch.object(self.blindpay._api, "_request") as mock_request:
151+
mock_request.return_value = {"data": mocked_sandbox, "error": None}
152+
153+
response = self.blindpay.sandbox.update({"sandbox_id": "sb_000000000000", "name": "Updated Sandbox"})
154+
155+
assert response["error"] is None
156+
assert response["data"] == mocked_sandbox
157+
mock_request.assert_called_once_with(
158+
"PATCH", "/instances/in_000000000000/sandbox/sb_000000000000", {"name": "Updated Sandbox"}
159+
)
160+
161+
def test_delete(self):
162+
with patch.object(self.blindpay._api, "_request") as mock_request:
163+
mock_request.return_value = {"data": {"success": True}, "error": None}
164+
165+
response = self.blindpay.sandbox.delete("sb_000000000000")
166+
167+
assert response["error"] is None
168+
mock_request.assert_called_once_with("DELETE", "/instances/in_000000000000/sandbox/sb_000000000000", None)

0 commit comments

Comments
 (0)