Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
737 changes: 737 additions & 0 deletions .claude-pr/CLAUDE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "blindpay"
version = "1.3.0"
version = "1.4.0"
description = "Official Python SDK for the Blindpay API — Global payments infrastructure"
readme = "README.md"
authors = [{ name = "Blindpay", email = "alves@blindpay.com" }]
Expand Down
2 changes: 1 addition & 1 deletion src/blindpay/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "1.3.0"
__version__ = "1.4.0"

from ._internal.exceptions import BlindPayError
from .client import BlindPay, BlindPaySync
Expand Down
15 changes: 14 additions & 1 deletion src/blindpay/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from blindpay.resources.payouts.payouts import PayoutsResource, PayoutsResourceSync
from blindpay.resources.quotes.quotes import QuotesResource, QuotesResourceSync
from blindpay.resources.receivers.receivers import ReceiversResource, ReceiversResourceSync
from blindpay.resources.sandbox.sandbox import SandboxResource, SandboxResourceSync
from blindpay.resources.terms_of_service.terms_of_service import (
TermsOfServiceResource,
TermsOfServiceResourceSync,
Expand All @@ -38,7 +39,7 @@
from blindpay.resources.wallets.offramp import OfframpWalletsResource, OfframpWalletsResourceSync
from blindpay.resources.webhooks.webhooks import WebhookEndpointsResource, WebhookEndpointsResourceSync

__version__ = "1.3.0"
__version__ = "1.4.0"

T = TypeVar("T")

Expand Down Expand Up @@ -308,6 +309,12 @@ def payouts(self) -> "PayoutsResource":
def receivers(self) -> _ReceiversNamespace:
return _ReceiversNamespace(self._instance_id, self._api)

@cached_property
def sandbox(self) -> "SandboxResource":
from blindpay.resources.sandbox import create_sandbox_resource

return create_sandbox_resource(self._instance_id, self._api)

@cached_property
def virtual_accounts(self) -> "VirtualAccountsResource":
from blindpay.resources.virtual_accounts import create_virtual_accounts_resource
Expand Down Expand Up @@ -526,6 +533,12 @@ def payouts(self) -> "PayoutsResourceSync":
def receivers(self) -> _ReceiversNamespaceSync:
return _ReceiversNamespaceSync(self._instance_id, self._api)

@cached_property
def sandbox(self) -> "SandboxResourceSync":
from blindpay.resources.sandbox import create_sandbox_resource_sync

return create_sandbox_resource_sync(self._instance_id, self._api)

@cached_property
def virtual_accounts(self) -> "VirtualAccountsResourceSync":
from blindpay.resources.virtual_accounts import create_virtual_accounts_resource_sync
Expand Down
3 changes: 3 additions & 0 deletions src/blindpay/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .payouts import create_payouts_resource
from .quotes import create_quotes_resource
from .receivers import create_receivers_resource
from .sandbox import create_sandbox_resource, create_sandbox_resource_sync
from .terms_of_service import create_terms_of_service_resource
from .transfers import create_transfers_resource
from .virtual_accounts import create_virtual_accounts_resource
Expand All @@ -28,6 +29,8 @@
"create_payouts_resource",
"create_quotes_resource",
"create_receivers_resource",
"create_sandbox_resource",
"create_sandbox_resource_sync",
"create_terms_of_service_resource",
"create_transfers_resource",
"create_virtual_accounts_resource",
Expand Down
25 changes: 25 additions & 0 deletions src/blindpay/resources/sandbox/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from .sandbox import (
CreateSandboxInput,
CreateSandboxResponse,
ListSandboxResponse,
SandboxItem,
SandboxResource,
SandboxResourceSync,
SandboxStatus,
UpdateSandboxInput,
create_sandbox_resource,
create_sandbox_resource_sync,
)

__all__ = [
"create_sandbox_resource",
"create_sandbox_resource_sync",
"SandboxResource",
"SandboxResourceSync",
"SandboxItem",
"SandboxStatus",
"CreateSandboxInput",
"CreateSandboxResponse",
"ListSandboxResponse",
"UpdateSandboxInput",
]
105 changes: 105 additions & 0 deletions src/blindpay/resources/sandbox/sandbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from typing import Dict, List, Literal, Optional

from typing_extensions import TypedDict

from ..._internal.api_client import InternalApiClient, InternalApiClientSync
from ...types import BlindpayApiResponse

SandboxStatus = Literal["active", "inactive"]


class SandboxItem(TypedDict):
id: str
name: str
status: SandboxStatus
metadata: Optional[Dict[str, str]]
created_at: Optional[str]
updated_at: Optional[str]


class CreateSandboxInput(TypedDict, total=False):
name: str
metadata: Dict[str, str]


class CreateSandboxResponse(TypedDict):
id: str
name: str
status: SandboxStatus
metadata: Optional[Dict[str, str]]


class ListSandboxResponse(TypedDict):
data: List[SandboxItem]


class _UpdateSandboxInputRequired(TypedDict):
sandbox_id: str


class UpdateSandboxInput(_UpdateSandboxInputRequired, total=False):
name: str
metadata: Dict[str, str]


# --- Async resource ---


class SandboxResource:
def __init__(self, instance_id: str, client: InternalApiClient):
self._instance_id = instance_id
self._client = client

async def list(self) -> BlindpayApiResponse[List[SandboxItem]]:
return await self._client.get(f"/instances/{self._instance_id}/sandbox")

async def get(self, sandbox_id: str) -> BlindpayApiResponse[SandboxItem]:
return await self._client.get(f"/instances/{self._instance_id}/sandbox/{sandbox_id}")

async def create(self, data: CreateSandboxInput) -> BlindpayApiResponse[CreateSandboxResponse]:
return await self._client.post(f"/instances/{self._instance_id}/sandbox", data)

async def update(self, data: UpdateSandboxInput) -> BlindpayApiResponse[SandboxItem]:
sandbox_id = data["sandbox_id"]
payload = {k: v for k, v in data.items() if k != "sandbox_id"}
return await self._client.patch(f"/instances/{self._instance_id}/sandbox/{sandbox_id}", payload)

async def delete(self, sandbox_id: str) -> BlindpayApiResponse[None]:
return await self._client.delete(f"/instances/{self._instance_id}/sandbox/{sandbox_id}")


# --- Sync resource ---


class SandboxResourceSync:
def __init__(self, instance_id: str, client: InternalApiClientSync):
self._instance_id = instance_id
self._client = client

def list(self) -> BlindpayApiResponse[List[SandboxItem]]:
return self._client.get(f"/instances/{self._instance_id}/sandbox")

def get(self, sandbox_id: str) -> BlindpayApiResponse[SandboxItem]:
return self._client.get(f"/instances/{self._instance_id}/sandbox/{sandbox_id}")

def create(self, data: CreateSandboxInput) -> BlindpayApiResponse[CreateSandboxResponse]:
return self._client.post(f"/instances/{self._instance_id}/sandbox", data)

def update(self, data: UpdateSandboxInput) -> BlindpayApiResponse[SandboxItem]:
sandbox_id = data["sandbox_id"]
payload = {k: v for k, v in data.items() if k != "sandbox_id"}
return self._client.patch(f"/instances/{self._instance_id}/sandbox/{sandbox_id}", payload)

def delete(self, sandbox_id: str) -> BlindpayApiResponse[None]:
return self._client.delete(f"/instances/{self._instance_id}/sandbox/{sandbox_id}")


# --- Factory functions ---


def create_sandbox_resource(instance_id: str, client: InternalApiClient) -> SandboxResource:
return SandboxResource(instance_id, client)


def create_sandbox_resource_sync(instance_id: str, client: InternalApiClientSync) -> SandboxResourceSync:
return SandboxResourceSync(instance_id, client)
145 changes: 145 additions & 0 deletions tests/resources/test_sandbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
from typing import Any
from unittest.mock import patch

import pytest

from blindpay import BlindPay, BlindPaySync


class TestSandbox:
@pytest.fixture(autouse=True)
def setup(self):
self.blindpay = BlindPay(api_key="test-key", instance_id="in_000000000000")

@pytest.mark.asyncio
async def test_list(self):
mocked_data: Any = [{"id": "sb_000000000000", "name": "My Sandbox Item", "status": "active"}]

with patch.object(self.blindpay._api, "_request") as mock_request:
mock_request.return_value = {"data": mocked_data, "error": None}

response = await self.blindpay.sandbox.list()

assert response["error"] is None
assert response["data"] == mocked_data
mock_request.assert_called_once_with("GET", "/instances/in_000000000000/sandbox")

@pytest.mark.asyncio
async def test_get(self):
mocked_data: Any = {"id": "sb_000000000000", "name": "My Sandbox Item", "status": "active"}

with patch.object(self.blindpay._api, "_request") as mock_request:
mock_request.return_value = {"data": mocked_data, "error": None}

response = await self.blindpay.sandbox.get("sb_000000000000")

assert response["error"] is None
assert response["data"] == mocked_data
mock_request.assert_called_once_with("GET", "/instances/in_000000000000/sandbox/sb_000000000000")

@pytest.mark.asyncio
async def test_create(self):
mocked_data: Any = {"id": "sb_000000000000", "name": "My Sandbox Item", "status": "active"}

with patch.object(self.blindpay._api, "_request") as mock_request:
mock_request.return_value = {"data": mocked_data, "error": None}

response = await self.blindpay.sandbox.create({"name": "My Sandbox Item"})

assert response["error"] is None
assert response["data"] == mocked_data
mock_request.assert_called_once_with(
"POST", "/instances/in_000000000000/sandbox", {"name": "My Sandbox Item"}
)

@pytest.mark.asyncio
async def test_update(self):
mocked_data: Any = {"id": "sb_000000000000", "name": "Updated Name", "status": "active"}

with patch.object(self.blindpay._api, "_request") as mock_request:
mock_request.return_value = {"data": mocked_data, "error": None}

response = await self.blindpay.sandbox.update({"sandbox_id": "sb_000000000000", "name": "Updated Name"})

assert response["error"] is None
assert response["data"] == mocked_data
mock_request.assert_called_once_with(
"PATCH", "/instances/in_000000000000/sandbox/sb_000000000000", {"name": "Updated Name"}
)

@pytest.mark.asyncio
async def test_delete(self):
with patch.object(self.blindpay._api, "_request") as mock_request:
mock_request.return_value = {"data": None, "error": None}

response = await self.blindpay.sandbox.delete("sb_000000000000")

assert response["error"] is None
mock_request.assert_called_once_with("DELETE", "/instances/in_000000000000/sandbox/sb_000000000000", None)


class TestSandboxSync:
@pytest.fixture(autouse=True)
def setup(self):
self.blindpay = BlindPaySync(api_key="test-key", instance_id="in_000000000000")

def test_list(self):
mocked_data: Any = [{"id": "sb_000000000000", "name": "My Sandbox Item", "status": "active"}]

with patch.object(self.blindpay._api, "_request") as mock_request:
mock_request.return_value = {"data": mocked_data, "error": None}

response = self.blindpay.sandbox.list()

assert response["error"] is None
assert response["data"] == mocked_data
mock_request.assert_called_once_with("GET", "/instances/in_000000000000/sandbox")

def test_get(self):
mocked_data: Any = {"id": "sb_000000000000", "name": "My Sandbox Item", "status": "active"}

with patch.object(self.blindpay._api, "_request") as mock_request:
mock_request.return_value = {"data": mocked_data, "error": None}

response = self.blindpay.sandbox.get("sb_000000000000")

assert response["error"] is None
assert response["data"] == mocked_data
mock_request.assert_called_once_with("GET", "/instances/in_000000000000/sandbox/sb_000000000000")

def test_create(self):
mocked_data: Any = {"id": "sb_000000000000", "name": "My Sandbox Item", "status": "active"}

with patch.object(self.blindpay._api, "_request") as mock_request:
mock_request.return_value = {"data": mocked_data, "error": None}

response = self.blindpay.sandbox.create({"name": "My Sandbox Item"})

assert response["error"] is None
assert response["data"] == mocked_data
mock_request.assert_called_once_with(
"POST", "/instances/in_000000000000/sandbox", {"name": "My Sandbox Item"}
)

def test_update(self):
mocked_data: Any = {"id": "sb_000000000000", "name": "Updated Name", "status": "active"}

with patch.object(self.blindpay._api, "_request") as mock_request:
mock_request.return_value = {"data": mocked_data, "error": None}

response = self.blindpay.sandbox.update({"sandbox_id": "sb_000000000000", "name": "Updated Name"})

assert response["error"] is None
assert response["data"] == mocked_data
mock_request.assert_called_once_with(
"PATCH", "/instances/in_000000000000/sandbox/sb_000000000000", {"name": "Updated Name"}
)

def test_delete(self):
with patch.object(self.blindpay._api, "_request") as mock_request:
mock_request.return_value = {"data": None, "error": None}

response = self.blindpay.sandbox.delete("sb_000000000000")

assert response["error"] is None
mock_request.assert_called_once_with("DELETE", "/instances/in_000000000000/sandbox/sb_000000000000", None)
Loading