epusdt 0.2.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.
- epusdt/__init__.py +78 -0
- epusdt/client.py +463 -0
- epusdt/exceptions.py +68 -0
- epusdt/models.py +334 -0
- epusdt/py.typed +1 -0
- epusdt/retry.py +42 -0
- epusdt/signature.py +123 -0
- epusdt-0.2.0.dist-info/METADATA +259 -0
- epusdt-0.2.0.dist-info/RECORD +12 -0
- epusdt-0.2.0.dist-info/WHEEL +5 -0
- epusdt-0.2.0.dist-info/licenses/LICENSE +22 -0
- epusdt-0.2.0.dist-info/top_level.txt +1 -0
epusdt/__init__.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from .client import EpusdtClient
|
|
2
|
+
from .exceptions import (
|
|
3
|
+
APIError,
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
ClientError,
|
|
6
|
+
EpusdtError,
|
|
7
|
+
NetworkError,
|
|
8
|
+
RequestTimeoutError,
|
|
9
|
+
ServerError,
|
|
10
|
+
SignatureError,
|
|
11
|
+
ValidationError,
|
|
12
|
+
)
|
|
13
|
+
from .models import (
|
|
14
|
+
CheckStatusResponse,
|
|
15
|
+
CheckoutOrder,
|
|
16
|
+
CreateOrderResponse,
|
|
17
|
+
EPayRedirectResponse,
|
|
18
|
+
EpayCallback,
|
|
19
|
+
EpayDefaults,
|
|
20
|
+
GmpayCallback,
|
|
21
|
+
ManualPaymentResponse,
|
|
22
|
+
Network,
|
|
23
|
+
OkpayConfig,
|
|
24
|
+
OrderStatus,
|
|
25
|
+
PaymentType,
|
|
26
|
+
PublicConfig,
|
|
27
|
+
SiteConfig,
|
|
28
|
+
SupportedAsset,
|
|
29
|
+
Token,
|
|
30
|
+
TradeStatus,
|
|
31
|
+
)
|
|
32
|
+
from .signature import (
|
|
33
|
+
build_epay_signing_string,
|
|
34
|
+
build_gmpay_signing_string,
|
|
35
|
+
generate_epay_signature,
|
|
36
|
+
generate_gmpay_signature,
|
|
37
|
+
verify_epay_signature,
|
|
38
|
+
verify_gmpay_signature,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"APIError",
|
|
43
|
+
"AuthenticationError",
|
|
44
|
+
"CheckStatusResponse",
|
|
45
|
+
"CheckoutOrder",
|
|
46
|
+
"ClientError",
|
|
47
|
+
"CreateOrderResponse",
|
|
48
|
+
"EPayRedirectResponse",
|
|
49
|
+
"EpayCallback",
|
|
50
|
+
"EpayDefaults",
|
|
51
|
+
"EpusdtClient",
|
|
52
|
+
"EpusdtError",
|
|
53
|
+
"GmpayCallback",
|
|
54
|
+
"ManualPaymentResponse",
|
|
55
|
+
"Network",
|
|
56
|
+
"NetworkError",
|
|
57
|
+
"OkpayConfig",
|
|
58
|
+
"OrderStatus",
|
|
59
|
+
"PaymentType",
|
|
60
|
+
"PublicConfig",
|
|
61
|
+
"RequestTimeoutError",
|
|
62
|
+
"ServerError",
|
|
63
|
+
"SignatureError",
|
|
64
|
+
"SiteConfig",
|
|
65
|
+
"SupportedAsset",
|
|
66
|
+
"Token",
|
|
67
|
+
"TradeStatus",
|
|
68
|
+
"ValidationError",
|
|
69
|
+
"build_epay_signing_string",
|
|
70
|
+
"build_gmpay_signing_string",
|
|
71
|
+
"generate_epay_signature",
|
|
72
|
+
"generate_gmpay_signature",
|
|
73
|
+
"verify_epay_signature",
|
|
74
|
+
"verify_gmpay_signature",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
__version__ = "0.2.0"
|
|
78
|
+
__url__ = "https://github.com/Yufeifeio/epusdt-python-sdk"
|
epusdt/client.py
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any, Mapping, MutableMapping, Optional
|
|
8
|
+
from urllib.parse import urlencode, urljoin, urlparse
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from .exceptions import (
|
|
13
|
+
APIError,
|
|
14
|
+
AuthenticationError,
|
|
15
|
+
ClientError,
|
|
16
|
+
NetworkError,
|
|
17
|
+
RequestTimeoutError,
|
|
18
|
+
ServerError,
|
|
19
|
+
SignatureError,
|
|
20
|
+
ValidationError,
|
|
21
|
+
)
|
|
22
|
+
from .models import (
|
|
23
|
+
CheckStatusResponse,
|
|
24
|
+
CheckoutOrder,
|
|
25
|
+
CreateOrderResponse,
|
|
26
|
+
EPayRedirectResponse,
|
|
27
|
+
EpayCallback,
|
|
28
|
+
GmpayCallback,
|
|
29
|
+
ManualPaymentResponse,
|
|
30
|
+
PublicConfig,
|
|
31
|
+
)
|
|
32
|
+
from .retry import call_with_retry
|
|
33
|
+
from .signature import (
|
|
34
|
+
generate_epay_signature,
|
|
35
|
+
generate_gmpay_signature,
|
|
36
|
+
stringify_params,
|
|
37
|
+
verify_epay_signature,
|
|
38
|
+
verify_gmpay_signature,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
USER_AGENT = "epusdt/0.2.0"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _require_text(name: str, value: Any) -> str:
|
|
47
|
+
if isinstance(value, Enum):
|
|
48
|
+
value = value.value
|
|
49
|
+
if value is None:
|
|
50
|
+
raise ValidationError(f"{name} is required")
|
|
51
|
+
text = str(value).strip()
|
|
52
|
+
if not text:
|
|
53
|
+
raise ValidationError(f"{name} is required")
|
|
54
|
+
return text
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _optional_text(name: str, value: Any) -> Optional[str]:
|
|
58
|
+
if value is None:
|
|
59
|
+
return None
|
|
60
|
+
if isinstance(value, Enum):
|
|
61
|
+
value = value.value
|
|
62
|
+
text = str(value).strip()
|
|
63
|
+
if not text:
|
|
64
|
+
return None
|
|
65
|
+
return text
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _validate_url(name: str, value: Optional[str]) -> Optional[str]:
|
|
69
|
+
if value is None:
|
|
70
|
+
return None
|
|
71
|
+
parsed = urlparse(value)
|
|
72
|
+
if parsed.scheme not in ("http", "https") or not parsed.netloc:
|
|
73
|
+
raise ValidationError(f"{name} must be a valid http(s) URL")
|
|
74
|
+
return value
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _is_numeric_pid(value: str) -> bool:
|
|
78
|
+
return value.isdigit()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _normalize_amount(name: str, value: Any) -> Any:
|
|
82
|
+
if isinstance(value, bool):
|
|
83
|
+
raise ValidationError(f"{name} must be a number")
|
|
84
|
+
if isinstance(value, Decimal):
|
|
85
|
+
numeric = value
|
|
86
|
+
elif isinstance(value, (int, float)):
|
|
87
|
+
numeric = Decimal(str(value))
|
|
88
|
+
else:
|
|
89
|
+
raise ValidationError(f"{name} must be a number")
|
|
90
|
+
if numeric <= Decimal("0.01"):
|
|
91
|
+
raise ValidationError(f"{name} must be greater than 0.01")
|
|
92
|
+
if numeric == numeric.to_integral():
|
|
93
|
+
return int(numeric)
|
|
94
|
+
return float(numeric)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _json_value(value: Any) -> Any:
|
|
98
|
+
if isinstance(value, Enum):
|
|
99
|
+
return value.value
|
|
100
|
+
if isinstance(value, Decimal):
|
|
101
|
+
if value == value.to_integral():
|
|
102
|
+
return int(value)
|
|
103
|
+
return float(value)
|
|
104
|
+
return value
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class EpusdtClient:
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
*,
|
|
111
|
+
base_url: str,
|
|
112
|
+
pid: Any,
|
|
113
|
+
secret_key: str,
|
|
114
|
+
timeout: float = 30.0,
|
|
115
|
+
max_retries: int = 2,
|
|
116
|
+
retry_delay: float = 0.5,
|
|
117
|
+
session: Optional[requests.Session] = None,
|
|
118
|
+
) -> None:
|
|
119
|
+
parsed = urlparse(base_url)
|
|
120
|
+
if parsed.scheme not in ("http", "https") or not parsed.netloc:
|
|
121
|
+
raise ValidationError("base_url must be a valid http(s) URL")
|
|
122
|
+
if timeout <= 0:
|
|
123
|
+
raise ValidationError("timeout must be greater than 0")
|
|
124
|
+
if max_retries < 0:
|
|
125
|
+
raise ValidationError("max_retries must be >= 0")
|
|
126
|
+
if retry_delay < 0:
|
|
127
|
+
raise ValidationError("retry_delay must be >= 0")
|
|
128
|
+
|
|
129
|
+
self.base_url = base_url.rstrip("/")
|
|
130
|
+
self.pid = _require_text("pid", pid)
|
|
131
|
+
self.secret_key = _require_text("secret_key", secret_key)
|
|
132
|
+
self.timeout = timeout
|
|
133
|
+
self.max_retries = max_retries
|
|
134
|
+
self.retry_delay = retry_delay
|
|
135
|
+
self.session = session or requests.Session()
|
|
136
|
+
self.session.headers.setdefault("Accept", "application/json")
|
|
137
|
+
self.session.headers.setdefault("User-Agent", USER_AGENT)
|
|
138
|
+
|
|
139
|
+
def create_order(
|
|
140
|
+
self,
|
|
141
|
+
*,
|
|
142
|
+
order_id: Any,
|
|
143
|
+
amount: Any,
|
|
144
|
+
currency: Any = "cny",
|
|
145
|
+
notify_url: str,
|
|
146
|
+
token: Optional[Any] = None,
|
|
147
|
+
network: Optional[Any] = None,
|
|
148
|
+
redirect_url: Optional[str] = None,
|
|
149
|
+
name: Optional[Any] = None,
|
|
150
|
+
payment_type: Optional[Any] = None,
|
|
151
|
+
use_form: bool = False,
|
|
152
|
+
) -> CreateOrderResponse:
|
|
153
|
+
token_text = _optional_text("token", token)
|
|
154
|
+
network_text = _optional_text("network", network)
|
|
155
|
+
if bool(token_text) != bool(network_text):
|
|
156
|
+
raise ValidationError("token and network must be both provided or both omitted")
|
|
157
|
+
|
|
158
|
+
payload: MutableMapping[str, Any] = {
|
|
159
|
+
"pid": self.pid,
|
|
160
|
+
"order_id": _require_text("order_id", order_id),
|
|
161
|
+
"currency": _require_text("currency", currency),
|
|
162
|
+
"amount": _normalize_amount("amount", amount),
|
|
163
|
+
"notify_url": _validate_url("notify_url", _require_text("notify_url", notify_url)),
|
|
164
|
+
}
|
|
165
|
+
if token_text:
|
|
166
|
+
payload["token"] = token_text
|
|
167
|
+
if network_text:
|
|
168
|
+
payload["network"] = network_text
|
|
169
|
+
redirect = _optional_text("redirect_url", redirect_url)
|
|
170
|
+
if redirect:
|
|
171
|
+
payload["redirect_url"] = _validate_url("redirect_url", redirect)
|
|
172
|
+
name_text = _optional_text("name", name)
|
|
173
|
+
if name_text:
|
|
174
|
+
payload["name"] = name_text
|
|
175
|
+
payment_type_text = _optional_text("payment_type", payment_type)
|
|
176
|
+
if payment_type_text:
|
|
177
|
+
if payment_type_text.lower() == "epay" and not _is_numeric_pid(self.pid):
|
|
178
|
+
raise ValidationError("payment_type=Epay requires a numeric pid")
|
|
179
|
+
payload["payment_type"] = payment_type_text
|
|
180
|
+
|
|
181
|
+
payload["signature"] = generate_gmpay_signature(payload, self.secret_key)
|
|
182
|
+
body = self._json_request(
|
|
183
|
+
"POST",
|
|
184
|
+
"/payments/gmpay/v1/order/create-transaction",
|
|
185
|
+
json_payload=payload if not use_form else None,
|
|
186
|
+
form_payload=payload if use_form else None,
|
|
187
|
+
)
|
|
188
|
+
return CreateOrderResponse.from_dict(body["data"])
|
|
189
|
+
|
|
190
|
+
def get_public_config(self) -> PublicConfig:
|
|
191
|
+
body = self._json_request("GET", "/payments/gmpay/v1/config")
|
|
192
|
+
return PublicConfig.from_dict(body["data"])
|
|
193
|
+
|
|
194
|
+
def get_checkout(self, trade_id: Any) -> CheckoutOrder:
|
|
195
|
+
body = self._json_request(
|
|
196
|
+
"GET",
|
|
197
|
+
f"/pay/checkout-counter-resp/{_require_text('trade_id', trade_id)}",
|
|
198
|
+
)
|
|
199
|
+
return CheckoutOrder.from_dict(body["data"])
|
|
200
|
+
|
|
201
|
+
def check_status(self, trade_id: Any) -> CheckStatusResponse:
|
|
202
|
+
body = self._json_request(
|
|
203
|
+
"GET",
|
|
204
|
+
f"/pay/check-status/{_require_text('trade_id', trade_id)}",
|
|
205
|
+
)
|
|
206
|
+
return CheckStatusResponse.from_dict(body["data"])
|
|
207
|
+
|
|
208
|
+
def switch_network(self, *, trade_id: Any, token: Any, network: Any) -> CheckoutOrder:
|
|
209
|
+
payload = {
|
|
210
|
+
"trade_id": _require_text("trade_id", trade_id),
|
|
211
|
+
"token": _require_text("token", token),
|
|
212
|
+
"network": _require_text("network", network),
|
|
213
|
+
}
|
|
214
|
+
body = self._json_request("POST", "/pay/switch-network", json_payload=payload)
|
|
215
|
+
return CheckoutOrder.from_dict(body["data"])
|
|
216
|
+
|
|
217
|
+
def submit_tx_hash(self, *, trade_id: Any, block_transaction_id: Any) -> ManualPaymentResponse:
|
|
218
|
+
payload = {
|
|
219
|
+
"block_transaction_id": _require_text("block_transaction_id", block_transaction_id),
|
|
220
|
+
}
|
|
221
|
+
body = self._json_request(
|
|
222
|
+
"POST",
|
|
223
|
+
f"/pay/submit-tx-hash/{_require_text('trade_id', trade_id)}",
|
|
224
|
+
json_payload=payload,
|
|
225
|
+
)
|
|
226
|
+
return ManualPaymentResponse.from_dict(body["data"])
|
|
227
|
+
|
|
228
|
+
def build_epay_params(
|
|
229
|
+
self,
|
|
230
|
+
*,
|
|
231
|
+
out_trade_no: Any,
|
|
232
|
+
money: Any,
|
|
233
|
+
notify_url: str,
|
|
234
|
+
return_url: Optional[str] = None,
|
|
235
|
+
name: Optional[Any] = None,
|
|
236
|
+
type: Any = "alipay",
|
|
237
|
+
token: Optional[Any] = None,
|
|
238
|
+
network: Optional[Any] = None,
|
|
239
|
+
currency: Optional[Any] = None,
|
|
240
|
+
sign_type: str = "MD5",
|
|
241
|
+
) -> dict[str, str]:
|
|
242
|
+
if not _is_numeric_pid(self.pid):
|
|
243
|
+
raise ValidationError("EPay compatibility mode requires a numeric pid")
|
|
244
|
+
|
|
245
|
+
params: MutableMapping[str, Any] = {
|
|
246
|
+
"pid": self.pid,
|
|
247
|
+
"money": _normalize_amount("money", money),
|
|
248
|
+
"out_trade_no": _require_text("out_trade_no", out_trade_no),
|
|
249
|
+
"notify_url": _validate_url("notify_url", _require_text("notify_url", notify_url)),
|
|
250
|
+
"type": _require_text("type", type),
|
|
251
|
+
}
|
|
252
|
+
if return_url:
|
|
253
|
+
params["return_url"] = _validate_url("return_url", _require_text("return_url", return_url))
|
|
254
|
+
name_text = _optional_text("name", name)
|
|
255
|
+
if name_text:
|
|
256
|
+
params["name"] = name_text
|
|
257
|
+
token_text = _optional_text("token", token)
|
|
258
|
+
if token_text:
|
|
259
|
+
params["token"] = token_text
|
|
260
|
+
network_text = _optional_text("network", network)
|
|
261
|
+
if network_text:
|
|
262
|
+
params["network"] = network_text
|
|
263
|
+
currency_text = _optional_text("currency", currency)
|
|
264
|
+
if currency_text:
|
|
265
|
+
params["currency"] = currency_text
|
|
266
|
+
params["sign_type"] = _require_text("sign_type", sign_type)
|
|
267
|
+
params["sign"] = generate_epay_signature(params, self.secret_key)
|
|
268
|
+
return dict(stringify_params(params))
|
|
269
|
+
|
|
270
|
+
def build_epay_redirect_url(self, **kwargs: Any) -> str:
|
|
271
|
+
params = self.build_epay_params(**kwargs)
|
|
272
|
+
query = urlencode(params)
|
|
273
|
+
return f"{self.base_url}/payments/epay/v1/order/create-transaction/submit.php?{query}"
|
|
274
|
+
|
|
275
|
+
def create_epay_order(self, *, method: str = "GET", **kwargs: Any) -> EPayRedirectResponse:
|
|
276
|
+
params = self.build_epay_params(**kwargs)
|
|
277
|
+
method_upper = method.upper()
|
|
278
|
+
if method_upper not in {"GET", "POST"}:
|
|
279
|
+
raise ValidationError("method must be GET or POST")
|
|
280
|
+
|
|
281
|
+
if method_upper == "GET":
|
|
282
|
+
response = self._request(
|
|
283
|
+
method_upper,
|
|
284
|
+
"/payments/epay/v1/order/create-transaction/submit.php",
|
|
285
|
+
params=params,
|
|
286
|
+
allow_redirects=False,
|
|
287
|
+
)
|
|
288
|
+
else:
|
|
289
|
+
response = self._request(
|
|
290
|
+
method_upper,
|
|
291
|
+
"/payments/epay/v1/order/create-transaction/submit.php",
|
|
292
|
+
data=params,
|
|
293
|
+
allow_redirects=False,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
if 300 <= response.status_code < 400 and response.headers.get("Location"):
|
|
297
|
+
location = response.headers["Location"]
|
|
298
|
+
return EPayRedirectResponse(
|
|
299
|
+
status_code=response.status_code,
|
|
300
|
+
location=location,
|
|
301
|
+
checkout_url=urljoin(f"{self.base_url}/", location),
|
|
302
|
+
params=params,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
payload = None
|
|
306
|
+
try:
|
|
307
|
+
payload = response.json()
|
|
308
|
+
except ValueError:
|
|
309
|
+
payload = None
|
|
310
|
+
self._raise_for_response(response, payload if isinstance(payload, Mapping) else None)
|
|
311
|
+
raise ClientError(
|
|
312
|
+
f"unexpected response status: {response.status_code}",
|
|
313
|
+
http_status=response.status_code,
|
|
314
|
+
response_text=response.text,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def verify_gmpay_callback(
|
|
318
|
+
self,
|
|
319
|
+
payload: Mapping[str, Any],
|
|
320
|
+
*,
|
|
321
|
+
secret_key: Optional[str] = None,
|
|
322
|
+
) -> bool:
|
|
323
|
+
return verify_gmpay_signature(payload, secret_key or self.secret_key)
|
|
324
|
+
|
|
325
|
+
def parse_gmpay_callback(
|
|
326
|
+
self,
|
|
327
|
+
payload: Mapping[str, Any],
|
|
328
|
+
*,
|
|
329
|
+
verify: bool = True,
|
|
330
|
+
secret_key: Optional[str] = None,
|
|
331
|
+
) -> GmpayCallback:
|
|
332
|
+
if verify and not self.verify_gmpay_callback(payload, secret_key=secret_key):
|
|
333
|
+
raise SignatureError("invalid GMPay callback signature")
|
|
334
|
+
return GmpayCallback.from_dict(payload)
|
|
335
|
+
|
|
336
|
+
def verify_epay_callback(
|
|
337
|
+
self,
|
|
338
|
+
params: Mapping[str, Any],
|
|
339
|
+
*,
|
|
340
|
+
secret_key: Optional[str] = None,
|
|
341
|
+
) -> bool:
|
|
342
|
+
return verify_epay_signature(params, secret_key or self.secret_key)
|
|
343
|
+
|
|
344
|
+
def parse_epay_callback(
|
|
345
|
+
self,
|
|
346
|
+
params: Mapping[str, Any],
|
|
347
|
+
*,
|
|
348
|
+
verify: bool = True,
|
|
349
|
+
secret_key: Optional[str] = None,
|
|
350
|
+
) -> EpayCallback:
|
|
351
|
+
if verify and not self.verify_epay_callback(params, secret_key=secret_key):
|
|
352
|
+
raise SignatureError("invalid EPay callback signature")
|
|
353
|
+
return EpayCallback.from_dict(params)
|
|
354
|
+
|
|
355
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> requests.Response:
|
|
356
|
+
url = f"{self.base_url}{path}"
|
|
357
|
+
|
|
358
|
+
def send() -> requests.Response:
|
|
359
|
+
try:
|
|
360
|
+
response = self.session.request(method, url, timeout=self.timeout, **kwargs)
|
|
361
|
+
except requests.Timeout as exc:
|
|
362
|
+
raise RequestTimeoutError(str(exc)) from exc
|
|
363
|
+
except requests.RequestException as exc:
|
|
364
|
+
raise NetworkError(str(exc)) from exc
|
|
365
|
+
if response.status_code >= 500:
|
|
366
|
+
raise ServerError(
|
|
367
|
+
f"gateway returned HTTP {response.status_code}",
|
|
368
|
+
http_status=response.status_code,
|
|
369
|
+
response_text=response.text,
|
|
370
|
+
)
|
|
371
|
+
return response
|
|
372
|
+
|
|
373
|
+
return call_with_retry(
|
|
374
|
+
send,
|
|
375
|
+
max_retries=self.max_retries,
|
|
376
|
+
retry_delay=self.retry_delay,
|
|
377
|
+
retry_name=f"{method} {path}",
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
def _json_request(
|
|
381
|
+
self,
|
|
382
|
+
method: str,
|
|
383
|
+
path: str,
|
|
384
|
+
*,
|
|
385
|
+
json_payload: Optional[Mapping[str, Any]] = None,
|
|
386
|
+
form_payload: Optional[Mapping[str, Any]] = None,
|
|
387
|
+
) -> Mapping[str, Any]:
|
|
388
|
+
kwargs: dict[str, Any] = {}
|
|
389
|
+
if json_payload is not None:
|
|
390
|
+
kwargs["json"] = {key: _json_value(value) for key, value in json_payload.items()}
|
|
391
|
+
if form_payload is not None:
|
|
392
|
+
kwargs["data"] = stringify_params(form_payload)
|
|
393
|
+
response = self._request(method, path, **kwargs)
|
|
394
|
+
return self._parse_json_response(response)
|
|
395
|
+
|
|
396
|
+
def _parse_json_response(self, response: requests.Response) -> Mapping[str, Any]:
|
|
397
|
+
try:
|
|
398
|
+
payload = response.json()
|
|
399
|
+
except ValueError as exc:
|
|
400
|
+
if response.status_code >= 400:
|
|
401
|
+
self._raise_for_response(response)
|
|
402
|
+
raise ClientError(
|
|
403
|
+
"gateway did not return valid JSON",
|
|
404
|
+
http_status=response.status_code,
|
|
405
|
+
response_text=response.text,
|
|
406
|
+
) from exc
|
|
407
|
+
|
|
408
|
+
if not isinstance(payload, dict):
|
|
409
|
+
raise ClientError(
|
|
410
|
+
"gateway JSON response must be an object",
|
|
411
|
+
http_status=response.status_code,
|
|
412
|
+
response_text=json.dumps(payload, ensure_ascii=False),
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
if response.status_code >= 400:
|
|
416
|
+
self._raise_for_response(response, payload)
|
|
417
|
+
|
|
418
|
+
business_code = payload.get("status_code")
|
|
419
|
+
if business_code != 200:
|
|
420
|
+
raise APIError(
|
|
421
|
+
str(payload.get("message", "epusdt API error")),
|
|
422
|
+
business_code=int(business_code) if business_code is not None else None,
|
|
423
|
+
http_status=response.status_code,
|
|
424
|
+
response=payload,
|
|
425
|
+
)
|
|
426
|
+
return payload
|
|
427
|
+
|
|
428
|
+
def _raise_for_response(
|
|
429
|
+
self,
|
|
430
|
+
response: requests.Response,
|
|
431
|
+
payload: Optional[Mapping[str, Any]] = None,
|
|
432
|
+
) -> None:
|
|
433
|
+
message = ""
|
|
434
|
+
business_code = None
|
|
435
|
+
if isinstance(payload, Mapping):
|
|
436
|
+
message = str(payload.get("message", "")).strip()
|
|
437
|
+
raw_code = payload.get("status_code")
|
|
438
|
+
if raw_code is not None:
|
|
439
|
+
try:
|
|
440
|
+
business_code = int(raw_code)
|
|
441
|
+
except (TypeError, ValueError):
|
|
442
|
+
business_code = None
|
|
443
|
+
if not message:
|
|
444
|
+
message = response.text.strip() or f"HTTP {response.status_code}"
|
|
445
|
+
|
|
446
|
+
if response.status_code == 401:
|
|
447
|
+
raise AuthenticationError(
|
|
448
|
+
message,
|
|
449
|
+
http_status=response.status_code,
|
|
450
|
+
response_text=response.text,
|
|
451
|
+
)
|
|
452
|
+
if payload is not None:
|
|
453
|
+
raise APIError(
|
|
454
|
+
message,
|
|
455
|
+
business_code=business_code,
|
|
456
|
+
http_status=response.status_code,
|
|
457
|
+
response=payload,
|
|
458
|
+
)
|
|
459
|
+
raise ClientError(
|
|
460
|
+
message,
|
|
461
|
+
http_status=response.status_code,
|
|
462
|
+
response_text=response.text,
|
|
463
|
+
)
|
epusdt/exceptions.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EpusdtError(Exception):
|
|
7
|
+
"""Base exception for this SDK."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ValidationError(EpusdtError):
|
|
11
|
+
"""Raised when user input is invalid before the request is sent."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SignatureError(EpusdtError):
|
|
15
|
+
"""Raised when callback signature verification fails."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NetworkError(EpusdtError):
|
|
19
|
+
"""Raised when the request cannot reach the gateway."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RequestTimeoutError(EpusdtError):
|
|
23
|
+
"""Raised when the gateway request times out."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class HTTPError(EpusdtError):
|
|
27
|
+
"""Base class for raw HTTP failures."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
message: str,
|
|
32
|
+
*,
|
|
33
|
+
http_status: Optional[int] = None,
|
|
34
|
+
response_text: Optional[str] = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
super().__init__(message)
|
|
37
|
+
self.http_status = http_status
|
|
38
|
+
self.response_text = response_text
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ClientError(HTTPError):
|
|
42
|
+
"""Raised for non-auth client-side HTTP errors."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AuthenticationError(ClientError):
|
|
46
|
+
"""Raised when the gateway rejects credentials or signature."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ServerError(HTTPError):
|
|
50
|
+
"""Raised for retryable 5xx gateway errors."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class APIError(EpusdtError):
|
|
54
|
+
"""Raised when the gateway returns a business error payload."""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
message: str,
|
|
59
|
+
*,
|
|
60
|
+
business_code: Optional[int] = None,
|
|
61
|
+
http_status: Optional[int] = None,
|
|
62
|
+
response: Optional[Any] = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
super().__init__(message)
|
|
65
|
+
self.business_code = business_code
|
|
66
|
+
self.http_status = http_status
|
|
67
|
+
self.response = response
|
|
68
|
+
|