decane-sdk 0.8.0__py3-none-any.whl
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.
- decane/__init__.py +45 -0
- decane/_internal/__init__.py +0 -0
- decane/_internal/http.py +195 -0
- decane/client.py +122 -0
- decane/errors.py +60 -0
- decane/resources/__init__.py +17 -0
- decane/resources/invoices.py +155 -0
- decane/resources/payment_links.py +81 -0
- decane/resources/payouts.py +141 -0
- decane/resources/subscriptions.py +123 -0
- decane/resources/swaps.py +84 -0
- decane/resources/wallets.py +100 -0
- decane/resources/webhooks.py +127 -0
- decane/types.py +311 -0
- decane_sdk-0.8.0.dist-info/METADATA +213 -0
- decane_sdk-0.8.0.dist-info/RECORD +18 -0
- decane_sdk-0.8.0.dist-info/WHEEL +4 -0
- decane_sdk-0.8.0.dist-info/licenses/LICENSE +21 -0
decane/__init__.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Decane — Official Python SDK for institutional liquidity rail for crypto settlement."""
|
|
2
|
+
|
|
3
|
+
from .client import Decane, AsyncDecane
|
|
4
|
+
from .errors import (
|
|
5
|
+
DecaneError,
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
NotFoundError,
|
|
8
|
+
ValidationError,
|
|
9
|
+
RateLimitError,
|
|
10
|
+
ServerError,
|
|
11
|
+
InvalidSignatureError,
|
|
12
|
+
)
|
|
13
|
+
from .types import (
|
|
14
|
+
Invoice,
|
|
15
|
+
InvoiceStatus,
|
|
16
|
+
Payment,
|
|
17
|
+
Payout,
|
|
18
|
+
PayoutStatus,
|
|
19
|
+
Wallet,
|
|
20
|
+
SwapQuote,
|
|
21
|
+
Swap,
|
|
22
|
+
WebhookEvent,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__version__ = "0.8.0"
|
|
26
|
+
__all__ = [
|
|
27
|
+
"Decane",
|
|
28
|
+
"AsyncDecane",
|
|
29
|
+
"DecaneError",
|
|
30
|
+
"AuthenticationError",
|
|
31
|
+
"NotFoundError",
|
|
32
|
+
"ValidationError",
|
|
33
|
+
"RateLimitError",
|
|
34
|
+
"ServerError",
|
|
35
|
+
"InvalidSignatureError",
|
|
36
|
+
"Invoice",
|
|
37
|
+
"InvoiceStatus",
|
|
38
|
+
"Payment",
|
|
39
|
+
"Payout",
|
|
40
|
+
"PayoutStatus",
|
|
41
|
+
"Wallet",
|
|
42
|
+
"SwapQuote",
|
|
43
|
+
"Swap",
|
|
44
|
+
"WebhookEvent",
|
|
45
|
+
]
|
|
File without changes
|
decane/_internal/http.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""HTTP transport layer for Decane SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from ..errors import (
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
DecaneError,
|
|
14
|
+
NotFoundError,
|
|
15
|
+
RateLimitError,
|
|
16
|
+
ServerError,
|
|
17
|
+
ValidationError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
DEFAULT_BASE_URL = "https://api.decane.io/api/v1/"
|
|
21
|
+
DEFAULT_TIMEOUT = 30.0
|
|
22
|
+
MAX_RETRIES = 3
|
|
23
|
+
RETRY_STATUSES = {429, 500, 502, 503, 504}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _build_headers(api_key: str, api_secret: str | None = None) -> dict[str, str]:
|
|
27
|
+
headers = {
|
|
28
|
+
"X-API-Key": api_key,
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
"User-Agent": "decane-python/0.8.0",
|
|
31
|
+
}
|
|
32
|
+
if api_secret:
|
|
33
|
+
headers["X-API-Secret"] = api_secret
|
|
34
|
+
return headers
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _with_idempotency(kwargs: dict[str, Any], key: str | None) -> dict[str, Any]:
|
|
38
|
+
"""Return kwargs with an Idempotency-Key header (generated if not given)."""
|
|
39
|
+
headers = dict(kwargs.get("headers") or {})
|
|
40
|
+
headers["Idempotency-Key"] = key or str(uuid.uuid4())
|
|
41
|
+
return {**kwargs, "headers": headers}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
45
|
+
"""Raise typed SDK errors from HTTP responses."""
|
|
46
|
+
if response.status_code < 400:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
body = response.json()
|
|
51
|
+
except Exception:
|
|
52
|
+
body = {"detail": response.text}
|
|
53
|
+
|
|
54
|
+
message = body.get("detail", body.get("message", body.get("error", str(body))))
|
|
55
|
+
code = body.get("code")
|
|
56
|
+
|
|
57
|
+
if response.status_code == 401:
|
|
58
|
+
raise AuthenticationError(message)
|
|
59
|
+
if response.status_code == 404:
|
|
60
|
+
raise NotFoundError(message)
|
|
61
|
+
if response.status_code == 422:
|
|
62
|
+
raise ValidationError(message, errors=body.get("errors"))
|
|
63
|
+
if response.status_code == 429:
|
|
64
|
+
retry_after = response.headers.get("Retry-After")
|
|
65
|
+
raise RateLimitError(message, retry_after=float(retry_after) if retry_after else None)
|
|
66
|
+
if response.status_code >= 500:
|
|
67
|
+
raise ServerError(message)
|
|
68
|
+
|
|
69
|
+
raise DecaneError(message, code=code, status=response.status_code)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class SyncHTTP:
|
|
73
|
+
"""Synchronous HTTP client with retry logic."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, api_key: str, base_url: str, timeout: float, api_secret: str | None = None):
|
|
76
|
+
self._client = httpx.Client(
|
|
77
|
+
base_url=base_url,
|
|
78
|
+
headers=_build_headers(api_key, api_secret),
|
|
79
|
+
timeout=timeout,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
83
|
+
last_exc: Exception | None = None
|
|
84
|
+
for attempt in range(MAX_RETRIES):
|
|
85
|
+
try:
|
|
86
|
+
response = self._client.request(method, path, **kwargs)
|
|
87
|
+
if response.status_code in RETRY_STATUSES and attempt < MAX_RETRIES - 1:
|
|
88
|
+
wait = (2**attempt) * 0.5
|
|
89
|
+
if response.status_code == 429:
|
|
90
|
+
retry_after = response.headers.get("Retry-After")
|
|
91
|
+
if retry_after:
|
|
92
|
+
wait = float(retry_after)
|
|
93
|
+
time.sleep(wait)
|
|
94
|
+
continue
|
|
95
|
+
_raise_for_status(response)
|
|
96
|
+
if response.status_code == 204:
|
|
97
|
+
return None
|
|
98
|
+
return response.json()
|
|
99
|
+
except (httpx.ConnectError, httpx.TimeoutException) as e:
|
|
100
|
+
last_exc = e
|
|
101
|
+
if attempt < MAX_RETRIES - 1:
|
|
102
|
+
time.sleep((2**attempt) * 0.5)
|
|
103
|
+
continue
|
|
104
|
+
raise DecaneError(f"Connection error: {e}", code="network_error") from e
|
|
105
|
+
if last_exc:
|
|
106
|
+
raise DecaneError(f"Max retries exceeded: {last_exc}", code="network_error") from last_exc
|
|
107
|
+
raise DecaneError("Unexpected error", code="unknown")
|
|
108
|
+
|
|
109
|
+
def get(self, path: str, **kwargs: Any) -> Any:
|
|
110
|
+
return self.request("GET", path, **kwargs)
|
|
111
|
+
|
|
112
|
+
def post(self, path: str, *, idempotency_key: str | None = None, **kwargs: Any) -> Any:
|
|
113
|
+
if idempotency_key is not None:
|
|
114
|
+
kwargs = _with_idempotency(kwargs, idempotency_key)
|
|
115
|
+
return self.request("POST", path, **kwargs)
|
|
116
|
+
|
|
117
|
+
def post_idempotent(self, path: str, *, idempotency_key: str | None = None, **kwargs: Any) -> Any:
|
|
118
|
+
"""POST with an Idempotency-Key header (random UUID if none given)."""
|
|
119
|
+
return self.request("POST", path, **_with_idempotency(kwargs, idempotency_key))
|
|
120
|
+
|
|
121
|
+
def put(self, path: str, **kwargs: Any) -> Any:
|
|
122
|
+
return self.request("PUT", path, **kwargs)
|
|
123
|
+
|
|
124
|
+
def patch(self, path: str, **kwargs: Any) -> Any:
|
|
125
|
+
return self.request("PATCH", path, **kwargs)
|
|
126
|
+
|
|
127
|
+
def delete(self, path: str, **kwargs: Any) -> Any:
|
|
128
|
+
return self.request("DELETE", path, **kwargs)
|
|
129
|
+
|
|
130
|
+
def close(self) -> None:
|
|
131
|
+
self._client.close()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class AsyncHTTP:
|
|
135
|
+
"""Async HTTP client with retry logic."""
|
|
136
|
+
|
|
137
|
+
def __init__(self, api_key: str, base_url: str, timeout: float, api_secret: str | None = None):
|
|
138
|
+
self._client = httpx.AsyncClient(
|
|
139
|
+
base_url=base_url,
|
|
140
|
+
headers=_build_headers(api_key, api_secret),
|
|
141
|
+
timeout=timeout,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
async def request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
145
|
+
import asyncio
|
|
146
|
+
|
|
147
|
+
last_exc: Exception | None = None
|
|
148
|
+
for attempt in range(MAX_RETRIES):
|
|
149
|
+
try:
|
|
150
|
+
response = await self._client.request(method, path, **kwargs)
|
|
151
|
+
if response.status_code in RETRY_STATUSES and attempt < MAX_RETRIES - 1:
|
|
152
|
+
wait = (2**attempt) * 0.5
|
|
153
|
+
if response.status_code == 429:
|
|
154
|
+
retry_after = response.headers.get("Retry-After")
|
|
155
|
+
if retry_after:
|
|
156
|
+
wait = float(retry_after)
|
|
157
|
+
await asyncio.sleep(wait)
|
|
158
|
+
continue
|
|
159
|
+
_raise_for_status(response)
|
|
160
|
+
if response.status_code == 204:
|
|
161
|
+
return None
|
|
162
|
+
return response.json()
|
|
163
|
+
except (httpx.ConnectError, httpx.TimeoutException) as e:
|
|
164
|
+
last_exc = e
|
|
165
|
+
if attempt < MAX_RETRIES - 1:
|
|
166
|
+
await asyncio.sleep((2**attempt) * 0.5)
|
|
167
|
+
continue
|
|
168
|
+
raise DecaneError(f"Connection error: {e}", code="network_error") from e
|
|
169
|
+
if last_exc:
|
|
170
|
+
raise DecaneError(f"Max retries exceeded: {last_exc}", code="network_error") from last_exc
|
|
171
|
+
raise DecaneError("Unexpected error", code="unknown")
|
|
172
|
+
|
|
173
|
+
async def get(self, path: str, **kwargs: Any) -> Any:
|
|
174
|
+
return await self.request("GET", path, **kwargs)
|
|
175
|
+
|
|
176
|
+
async def post(self, path: str, *, idempotency_key: str | None = None, **kwargs: Any) -> Any:
|
|
177
|
+
if idempotency_key is not None:
|
|
178
|
+
kwargs = _with_idempotency(kwargs, idempotency_key)
|
|
179
|
+
return await self.request("POST", path, **kwargs)
|
|
180
|
+
|
|
181
|
+
async def post_idempotent(self, path: str, *, idempotency_key: str | None = None, **kwargs: Any) -> Any:
|
|
182
|
+
"""POST with an Idempotency-Key header (random UUID if none given)."""
|
|
183
|
+
return await self.request("POST", path, **_with_idempotency(kwargs, idempotency_key))
|
|
184
|
+
|
|
185
|
+
async def put(self, path: str, **kwargs: Any) -> Any:
|
|
186
|
+
return await self.request("PUT", path, **kwargs)
|
|
187
|
+
|
|
188
|
+
async def patch(self, path: str, **kwargs: Any) -> Any:
|
|
189
|
+
return await self.request("PATCH", path, **kwargs)
|
|
190
|
+
|
|
191
|
+
async def delete(self, path: str, **kwargs: Any) -> Any:
|
|
192
|
+
return await self.request("DELETE", path, **kwargs)
|
|
193
|
+
|
|
194
|
+
async def close(self) -> None:
|
|
195
|
+
await self._client.aclose()
|
decane/client.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Decane client — the main entry point for the SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._internal.http import AsyncHTTP, SyncHTTP, DEFAULT_BASE_URL, DEFAULT_TIMEOUT
|
|
6
|
+
from .resources.invoices import AsyncInvoices, Invoices
|
|
7
|
+
from .resources.payouts import AsyncPayouts, Payouts
|
|
8
|
+
from .resources.swaps import AsyncSwaps, Swaps
|
|
9
|
+
from .resources.wallets import AsyncWallets, Wallets
|
|
10
|
+
from .resources.webhooks import Webhooks
|
|
11
|
+
from .resources.payment_links import AsyncPaymentLinks, PaymentLinks
|
|
12
|
+
from .resources.subscriptions import AsyncSubscriptions, Subscriptions
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Decane:
|
|
16
|
+
"""Synchronous Decane client.
|
|
17
|
+
|
|
18
|
+
Usage::
|
|
19
|
+
|
|
20
|
+
from decane import Decane
|
|
21
|
+
|
|
22
|
+
flow = Decane(api_key="gr_...", webhook_secret="gr_whsec_...")
|
|
23
|
+
|
|
24
|
+
# Create an invoice
|
|
25
|
+
invoice = flow.invoices.create(
|
|
26
|
+
amount=100, currency="USDT", network="tron",
|
|
27
|
+
)
|
|
28
|
+
print(invoice.wallet_address)
|
|
29
|
+
|
|
30
|
+
# Request a payout
|
|
31
|
+
payout = flow.payouts.create(
|
|
32
|
+
amount=50, currency="USDT", network="tron",
|
|
33
|
+
destination="TXyz...addr",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Verify a webhook
|
|
37
|
+
event = flow.webhooks.verify(payload, signature)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
api_key: str,
|
|
43
|
+
*,
|
|
44
|
+
api_secret: str | None = None,
|
|
45
|
+
webhook_secret: str | None = None,
|
|
46
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
47
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
48
|
+
):
|
|
49
|
+
if not api_key:
|
|
50
|
+
raise ValueError("api_key is required")
|
|
51
|
+
|
|
52
|
+
self._http = SyncHTTP(api_key=api_key, base_url=base_url, timeout=timeout, api_secret=api_secret)
|
|
53
|
+
|
|
54
|
+
self.invoices = Invoices(self._http)
|
|
55
|
+
self.payouts = Payouts(self._http)
|
|
56
|
+
self.wallets = Wallets(self._http)
|
|
57
|
+
self.swaps = Swaps(self._http)
|
|
58
|
+
self.webhooks = Webhooks(webhook_secret or "")
|
|
59
|
+
self.payment_links = PaymentLinks(self._http)
|
|
60
|
+
self.subscriptions = Subscriptions(self._http)
|
|
61
|
+
|
|
62
|
+
def close(self) -> None:
|
|
63
|
+
"""Close the underlying HTTP client."""
|
|
64
|
+
self._http.close()
|
|
65
|
+
|
|
66
|
+
def __enter__(self) -> Decane:
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
def __exit__(self, *args: object) -> None:
|
|
70
|
+
self.close()
|
|
71
|
+
|
|
72
|
+
def __repr__(self) -> str:
|
|
73
|
+
return "Decane(connected)"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class AsyncDecane:
|
|
77
|
+
"""Async Decane client.
|
|
78
|
+
|
|
79
|
+
Usage::
|
|
80
|
+
|
|
81
|
+
from decane import AsyncDecane
|
|
82
|
+
|
|
83
|
+
async with AsyncDecane(api_key="gr_...") as flow:
|
|
84
|
+
invoice = await flow.invoices.create(
|
|
85
|
+
amount=100, currency="USDT", network="tron",
|
|
86
|
+
)
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
api_key: str,
|
|
92
|
+
*,
|
|
93
|
+
api_secret: str | None = None,
|
|
94
|
+
webhook_secret: str | None = None,
|
|
95
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
96
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
97
|
+
):
|
|
98
|
+
if not api_key:
|
|
99
|
+
raise ValueError("api_key is required")
|
|
100
|
+
|
|
101
|
+
self._http = AsyncHTTP(api_key=api_key, base_url=base_url, timeout=timeout, api_secret=api_secret)
|
|
102
|
+
|
|
103
|
+
self.invoices = AsyncInvoices(self._http)
|
|
104
|
+
self.payouts = AsyncPayouts(self._http)
|
|
105
|
+
self.wallets = AsyncWallets(self._http)
|
|
106
|
+
self.swaps = AsyncSwaps(self._http)
|
|
107
|
+
self.webhooks = Webhooks(webhook_secret or "")
|
|
108
|
+
self.payment_links = AsyncPaymentLinks(self._http)
|
|
109
|
+
self.subscriptions = AsyncSubscriptions(self._http)
|
|
110
|
+
|
|
111
|
+
async def close(self) -> None:
|
|
112
|
+
"""Close the underlying HTTP client."""
|
|
113
|
+
await self._http.close()
|
|
114
|
+
|
|
115
|
+
async def __aenter__(self) -> AsyncDecane:
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
async def __aexit__(self, *args: object) -> None:
|
|
119
|
+
await self.close()
|
|
120
|
+
|
|
121
|
+
def __repr__(self) -> str:
|
|
122
|
+
return "AsyncDecane(connected)"
|
decane/errors.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Decane SDK error types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DecaneError(Exception):
|
|
7
|
+
"""Base error for all Decane SDK errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, code: str | None = None, status: int | None = None):
|
|
10
|
+
self.message = message
|
|
11
|
+
self.code = code
|
|
12
|
+
self.status = status
|
|
13
|
+
super().__init__(message)
|
|
14
|
+
|
|
15
|
+
def __repr__(self) -> str:
|
|
16
|
+
return f"{self.__class__.__name__}(message={self.message!r}, code={self.code!r}, status={self.status})"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AuthenticationError(DecaneError):
|
|
20
|
+
"""Invalid or missing API key."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, message: str = "Invalid or missing API key"):
|
|
23
|
+
super().__init__(message, code="authentication_error", status=401)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NotFoundError(DecaneError):
|
|
27
|
+
"""Resource not found."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, message: str = "Resource not found"):
|
|
30
|
+
super().__init__(message, code="not_found", status=404)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ValidationError(DecaneError):
|
|
34
|
+
"""Invalid request parameters."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, message: str = "Validation error", errors: list[dict] | None = None):
|
|
37
|
+
self.errors = errors or []
|
|
38
|
+
super().__init__(message, code="validation_error", status=422)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RateLimitError(DecaneError):
|
|
42
|
+
"""Too many requests."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, message: str = "Rate limit exceeded", retry_after: float | None = None):
|
|
45
|
+
self.retry_after = retry_after
|
|
46
|
+
super().__init__(message, code="rate_limit", status=429)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ServerError(DecaneError):
|
|
50
|
+
"""Server-side error."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, message: str = "Internal server error"):
|
|
53
|
+
super().__init__(message, code="server_error", status=500)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class InvalidSignatureError(DecaneError):
|
|
57
|
+
"""Webhook signature verification failed."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, message: str = "Invalid webhook signature"):
|
|
60
|
+
super().__init__(message, code="invalid_signature", status=None)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .invoices import Invoices, AsyncInvoices
|
|
2
|
+
from .payouts import Payouts, AsyncPayouts
|
|
3
|
+
from .wallets import Wallets, AsyncWallets
|
|
4
|
+
from .swaps import Swaps, AsyncSwaps
|
|
5
|
+
from .webhooks import Webhooks
|
|
6
|
+
from .payment_links import PaymentLinks, AsyncPaymentLinks
|
|
7
|
+
from .subscriptions import Subscriptions, AsyncSubscriptions
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Invoices", "AsyncInvoices",
|
|
11
|
+
"Payouts", "AsyncPayouts",
|
|
12
|
+
"Wallets", "AsyncWallets",
|
|
13
|
+
"Swaps", "AsyncSwaps",
|
|
14
|
+
"Webhooks",
|
|
15
|
+
"PaymentLinks", "AsyncPaymentLinks",
|
|
16
|
+
"Subscriptions", "AsyncSubscriptions",
|
|
17
|
+
]
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Invoice resource — create and manage payment invoices."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .._internal.http import AsyncHTTP, SyncHTTP
|
|
8
|
+
from ..types import Invoice, PaginatedList
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Invoices:
|
|
12
|
+
"""Synchronous invoice operations."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, http: SyncHTTP):
|
|
15
|
+
self._http = http
|
|
16
|
+
|
|
17
|
+
def create(
|
|
18
|
+
self,
|
|
19
|
+
amount: float,
|
|
20
|
+
currency: str = "USD",
|
|
21
|
+
*,
|
|
22
|
+
network: str | None = None,
|
|
23
|
+
crypto_currency: str | None = None,
|
|
24
|
+
merchant_order_id: str | None = None,
|
|
25
|
+
customer_email: str | None = None,
|
|
26
|
+
customer_name: str | None = None,
|
|
27
|
+
callback_url: str | None = None,
|
|
28
|
+
return_url: str | None = None,
|
|
29
|
+
metadata: dict[str, Any] | None = None,
|
|
30
|
+
idempotency_key: str | None = None,
|
|
31
|
+
) -> Invoice:
|
|
32
|
+
"""Create a payment invoice.
|
|
33
|
+
|
|
34
|
+
Omit ``network`` and ``crypto_currency`` to create a **flex invoice**
|
|
35
|
+
where the customer picks their preferred crypto on the payment page.
|
|
36
|
+
Supply both to lock the rate at creation time (fixed invoice).
|
|
37
|
+
|
|
38
|
+
``idempotency_key`` is sent as the ``Idempotency-Key`` header. If you
|
|
39
|
+
omit it, a random key is generated so automatic retries cannot create
|
|
40
|
+
a second invoice. Pass your own key to make retries from your side safe.
|
|
41
|
+
"""
|
|
42
|
+
payload: dict[str, Any] = {"amount": amount, "currency": currency}
|
|
43
|
+
if network is not None:
|
|
44
|
+
payload["network"] = network
|
|
45
|
+
if crypto_currency is not None:
|
|
46
|
+
payload["crypto_currency"] = crypto_currency
|
|
47
|
+
if merchant_order_id is not None:
|
|
48
|
+
payload["merchant_order_id"] = merchant_order_id
|
|
49
|
+
if customer_email is not None:
|
|
50
|
+
payload["customer_email"] = customer_email
|
|
51
|
+
if customer_name is not None:
|
|
52
|
+
payload["customer_name"] = customer_name
|
|
53
|
+
if callback_url is not None:
|
|
54
|
+
payload["callback_url"] = callback_url
|
|
55
|
+
if return_url is not None:
|
|
56
|
+
payload["return_url"] = return_url
|
|
57
|
+
if metadata is not None:
|
|
58
|
+
payload["metadata"] = metadata
|
|
59
|
+
|
|
60
|
+
data = self._http.post_idempotent("/invoices", json=payload, idempotency_key=idempotency_key)
|
|
61
|
+
return Invoice.from_dict(data)
|
|
62
|
+
|
|
63
|
+
def get(self, invoice_id: str) -> Invoice:
|
|
64
|
+
"""Get an invoice by ID."""
|
|
65
|
+
data = self._http.get(f"/invoices/{invoice_id}")
|
|
66
|
+
return Invoice.from_dict(data)
|
|
67
|
+
|
|
68
|
+
def list(
|
|
69
|
+
self,
|
|
70
|
+
*,
|
|
71
|
+
status: str | None = None,
|
|
72
|
+
currency: str | None = None,
|
|
73
|
+
page: int = 1,
|
|
74
|
+
limit: int = 50,
|
|
75
|
+
) -> PaginatedList:
|
|
76
|
+
"""List invoices with optional filters."""
|
|
77
|
+
params: dict[str, Any] = {"page": page, "limit": limit}
|
|
78
|
+
if status:
|
|
79
|
+
params["status"] = status
|
|
80
|
+
if currency:
|
|
81
|
+
params["currency"] = currency
|
|
82
|
+
data = self._http.get("/invoices", params=params)
|
|
83
|
+
return PaginatedList.from_dict(data, Invoice)
|
|
84
|
+
|
|
85
|
+
def cancel(self, invoice_id: str) -> Invoice:
|
|
86
|
+
"""Cancel a pending invoice."""
|
|
87
|
+
data = self._http.post(f"/invoices/{invoice_id}/cancel")
|
|
88
|
+
return Invoice.from_dict(data)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class AsyncInvoices:
|
|
92
|
+
"""Async invoice operations."""
|
|
93
|
+
|
|
94
|
+
def __init__(self, http: AsyncHTTP):
|
|
95
|
+
self._http = http
|
|
96
|
+
|
|
97
|
+
async def create(
|
|
98
|
+
self,
|
|
99
|
+
amount: float,
|
|
100
|
+
currency: str = "USD",
|
|
101
|
+
*,
|
|
102
|
+
network: str | None = None,
|
|
103
|
+
crypto_currency: str | None = None,
|
|
104
|
+
merchant_order_id: str | None = None,
|
|
105
|
+
customer_email: str | None = None,
|
|
106
|
+
customer_name: str | None = None,
|
|
107
|
+
callback_url: str | None = None,
|
|
108
|
+
return_url: str | None = None,
|
|
109
|
+
metadata: dict[str, Any] | None = None,
|
|
110
|
+
idempotency_key: str | None = None,
|
|
111
|
+
) -> Invoice:
|
|
112
|
+
payload: dict[str, Any] = {"amount": amount, "currency": currency}
|
|
113
|
+
if network is not None:
|
|
114
|
+
payload["network"] = network
|
|
115
|
+
if crypto_currency is not None:
|
|
116
|
+
payload["crypto_currency"] = crypto_currency
|
|
117
|
+
if merchant_order_id is not None:
|
|
118
|
+
payload["merchant_order_id"] = merchant_order_id
|
|
119
|
+
if customer_email is not None:
|
|
120
|
+
payload["customer_email"] = customer_email
|
|
121
|
+
if customer_name is not None:
|
|
122
|
+
payload["customer_name"] = customer_name
|
|
123
|
+
if callback_url is not None:
|
|
124
|
+
payload["callback_url"] = callback_url
|
|
125
|
+
if return_url is not None:
|
|
126
|
+
payload["return_url"] = return_url
|
|
127
|
+
if metadata is not None:
|
|
128
|
+
payload["metadata"] = metadata
|
|
129
|
+
|
|
130
|
+
data = await self._http.post_idempotent("/invoices", json=payload, idempotency_key=idempotency_key)
|
|
131
|
+
return Invoice.from_dict(data)
|
|
132
|
+
|
|
133
|
+
async def get(self, invoice_id: str) -> Invoice:
|
|
134
|
+
data = await self._http.get(f"/invoices/{invoice_id}")
|
|
135
|
+
return Invoice.from_dict(data)
|
|
136
|
+
|
|
137
|
+
async def list(
|
|
138
|
+
self,
|
|
139
|
+
*,
|
|
140
|
+
status: str | None = None,
|
|
141
|
+
currency: str | None = None,
|
|
142
|
+
page: int = 1,
|
|
143
|
+
limit: int = 50,
|
|
144
|
+
) -> PaginatedList:
|
|
145
|
+
params: dict[str, Any] = {"page": page, "limit": limit}
|
|
146
|
+
if status:
|
|
147
|
+
params["status"] = status
|
|
148
|
+
if currency:
|
|
149
|
+
params["currency"] = currency
|
|
150
|
+
data = await self._http.get("/invoices", params=params)
|
|
151
|
+
return PaginatedList.from_dict(data, Invoice)
|
|
152
|
+
|
|
153
|
+
async def cancel(self, invoice_id: str) -> Invoice:
|
|
154
|
+
data = await self._http.post(f"/invoices/{invoice_id}/cancel")
|
|
155
|
+
return Invoice.from_dict(data)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Payment link resource — create and manage hosted payment links."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .._internal.http import AsyncHTTP, SyncHTTP
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PaymentLinks:
|
|
11
|
+
"""Synchronous payment link operations."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, http: SyncHTTP):
|
|
14
|
+
self._http = http
|
|
15
|
+
|
|
16
|
+
def create(
|
|
17
|
+
self,
|
|
18
|
+
amount: float,
|
|
19
|
+
currency: str,
|
|
20
|
+
*,
|
|
21
|
+
title: str | None = None,
|
|
22
|
+
description: str | None = None,
|
|
23
|
+
redirect_url: str | None = None,
|
|
24
|
+
metadata: dict[str, Any] | None = None,
|
|
25
|
+
) -> dict[str, Any]:
|
|
26
|
+
payload: dict[str, Any] = {"amount": amount, "currency": currency}
|
|
27
|
+
if title is not None:
|
|
28
|
+
payload["title"] = title
|
|
29
|
+
if description is not None:
|
|
30
|
+
payload["description"] = description
|
|
31
|
+
if redirect_url is not None:
|
|
32
|
+
payload["redirect_url"] = redirect_url
|
|
33
|
+
if metadata is not None:
|
|
34
|
+
payload["metadata"] = metadata
|
|
35
|
+
return self._http.post("/merchants/me/payment-links", json=payload)
|
|
36
|
+
|
|
37
|
+
def list(self, *, page: int = 1, limit: int = 50) -> list[dict[str, Any]]:
|
|
38
|
+
return self._http.get("/merchants/me/payment-links", params={"page": page, "limit": limit})
|
|
39
|
+
|
|
40
|
+
def update(self, link_id: str, **fields: Any) -> dict[str, Any]:
|
|
41
|
+
return self._http.patch(f"/merchants/me/payment-links/{link_id}", json=fields)
|
|
42
|
+
|
|
43
|
+
def delete(self, link_id: str) -> None:
|
|
44
|
+
self._http.delete(f"/merchants/me/payment-links/{link_id}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AsyncPaymentLinks:
|
|
48
|
+
"""Async payment link operations."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, http: AsyncHTTP):
|
|
51
|
+
self._http = http
|
|
52
|
+
|
|
53
|
+
async def create(
|
|
54
|
+
self,
|
|
55
|
+
amount: float,
|
|
56
|
+
currency: str,
|
|
57
|
+
*,
|
|
58
|
+
title: str | None = None,
|
|
59
|
+
description: str | None = None,
|
|
60
|
+
redirect_url: str | None = None,
|
|
61
|
+
metadata: dict[str, Any] | None = None,
|
|
62
|
+
) -> dict[str, Any]:
|
|
63
|
+
payload: dict[str, Any] = {"amount": amount, "currency": currency}
|
|
64
|
+
if title is not None:
|
|
65
|
+
payload["title"] = title
|
|
66
|
+
if description is not None:
|
|
67
|
+
payload["description"] = description
|
|
68
|
+
if redirect_url is not None:
|
|
69
|
+
payload["redirect_url"] = redirect_url
|
|
70
|
+
if metadata is not None:
|
|
71
|
+
payload["metadata"] = metadata
|
|
72
|
+
return await self._http.post("/merchants/me/payment-links", json=payload)
|
|
73
|
+
|
|
74
|
+
async def list(self, *, page: int = 1, limit: int = 50) -> list[dict[str, Any]]:
|
|
75
|
+
return await self._http.get("/merchants/me/payment-links", params={"page": page, "limit": limit})
|
|
76
|
+
|
|
77
|
+
async def update(self, link_id: str, **fields: Any) -> dict[str, Any]:
|
|
78
|
+
return await self._http.patch(f"/merchants/me/payment-links/{link_id}", json=fields)
|
|
79
|
+
|
|
80
|
+
async def delete(self, link_id: str) -> None:
|
|
81
|
+
await self._http.delete(f"/merchants/me/payment-links/{link_id}")
|