payos 1.0.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.
Files changed (52) hide show
  1. payos/__init__.py +42 -0
  2. payos/_async_client.py +563 -0
  3. payos/_client.py +558 -0
  4. payos/_core/__init__.py +22 -0
  5. payos/_core/exceptions.py +169 -0
  6. payos/_core/models.py +28 -0
  7. payos/_core/pagination.py +320 -0
  8. payos/_core/request_options.py +123 -0
  9. payos/_crypto/__init__.py +5 -0
  10. payos/_crypto/provider.py +209 -0
  11. payos/_version.py +2 -0
  12. payos/resources/__init__.py +12 -0
  13. payos/resources/_base.py +23 -0
  14. payos/resources/v1/__init__.py +11 -0
  15. payos/resources/v1/payouts/__init__.py +5 -0
  16. payos/resources/v1/payouts/batch/__init__.py +5 -0
  17. payos/resources/v1/payouts/batch/batch.py +74 -0
  18. payos/resources/v1/payouts/payouts.py +212 -0
  19. payos/resources/v1/payouts_account/__init__.py +5 -0
  20. payos/resources/v1/payouts_account/payouts_account.py +50 -0
  21. payos/resources/v2/__init__.py +5 -0
  22. payos/resources/v2/payment_requests/__init__.py +5 -0
  23. payos/resources/v2/payment_requests/invoices/__init__.py +5 -0
  24. payos/resources/v2/payment_requests/invoices/invoices.py +91 -0
  25. payos/resources/v2/payment_requests/payment_requests.py +149 -0
  26. payos/resources/webhooks/__init__.py +5 -0
  27. payos/resources/webhooks/webhooks.py +144 -0
  28. payos/types/__init__.py +36 -0
  29. payos/types/common.py +12 -0
  30. payos/types/v1/__init__.py +14 -0
  31. payos/types/v1/payouts/__init__.py +15 -0
  32. payos/types/v1/payouts/batch/__init__.py +5 -0
  33. payos/types/v1/payouts/batch/batch.py +33 -0
  34. payos/types/v1/payouts/payouts.py +85 -0
  35. payos/types/v1/payouts_account/__init__.py +1 -0
  36. payos/types/v1/payouts_account/payouts_account.py +13 -0
  37. payos/types/v2/__init__.py +15 -0
  38. payos/types/v2/payment_requests/__init__.py +17 -0
  39. payos/types/v2/payment_requests/invoices/__init__.py +6 -0
  40. payos/types/v2/payment_requests/invoices/invoices.py +33 -0
  41. payos/types/v2/payment_requests/payment_requests.py +100 -0
  42. payos/types/webhooks/__init__.py +6 -0
  43. payos/types/webhooks/webhook.py +50 -0
  44. payos/utils/__init__.py +22 -0
  45. payos/utils/casting.py +50 -0
  46. payos/utils/env.py +7 -0
  47. payos/utils/json_utils.py +57 -0
  48. payos/utils/logs.py +63 -0
  49. payos/utils/validation.py +7 -0
  50. payos-1.0.0.dist-info/METADATA +330 -0
  51. payos-1.0.0.dist-info/RECORD +52 -0
  52. payos-1.0.0.dist-info/WHEEL +4 -0
payos/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ """payOS Python SDK.
2
+
3
+ A modern Python SDK for the payOS payment platform with sync/async support.
4
+ """
5
+
6
+ from . import types
7
+ from ._async_client import AsyncPayOS
8
+ from ._client import PayOS
9
+ from ._core.exceptions import (
10
+ APIError,
11
+ BadRequestError,
12
+ ConnectionError,
13
+ ConnectionTimeoutError,
14
+ ForbiddenError,
15
+ InternalServerError,
16
+ InvalidSignatureError,
17
+ NotFoundError,
18
+ PayOSError,
19
+ TooManyRequestsError,
20
+ UnauthorizedError,
21
+ WebhookError,
22
+ )
23
+ from ._version import __version__
24
+
25
+ __all__ = [
26
+ "types",
27
+ "PayOS",
28
+ "AsyncPayOS",
29
+ "PayOSError",
30
+ "APIError",
31
+ "ConnectionError",
32
+ "ConnectionTimeoutError",
33
+ "BadRequestError",
34
+ "UnauthorizedError",
35
+ "ForbiddenError",
36
+ "NotFoundError",
37
+ "TooManyRequestsError",
38
+ "InternalServerError",
39
+ "InvalidSignatureError",
40
+ "WebhookError",
41
+ "__version__",
42
+ ]
payos/_async_client.py ADDED
@@ -0,0 +1,563 @@
1
+ """Asynchronous payOS client."""
2
+
3
+ import asyncio
4
+ import email.utils
5
+ import json
6
+ import logging
7
+ import random
8
+ import time
9
+ from functools import cached_property
10
+ from types import TracebackType
11
+ from typing import Any, Optional, TypeVar, Union
12
+ from urllib.parse import urlencode, urljoin
13
+
14
+ from typing_extensions import Unpack
15
+
16
+ from ._core import (
17
+ FileDownloadResponse,
18
+ FinalRequestOptions,
19
+ )
20
+ from ._core.exceptions import (
21
+ APIError,
22
+ ConnectionError,
23
+ ConnectionTimeoutError,
24
+ InvalidSignatureError,
25
+ PayOSError,
26
+ )
27
+ from ._core.request_options import RequestOptions
28
+ from ._crypto import CryptoProvider
29
+ from ._version import __version__
30
+ from .utils import (
31
+ cast_to as cast_response_to,
32
+ get_env_var,
33
+ request_to_dict,
34
+ response_to_dict,
35
+ validate_positive_number,
36
+ )
37
+ from .utils.logs import SensitiveHeadersFilter
38
+
39
+ try:
40
+ import httpx
41
+ except ImportError:
42
+ raise ImportError("The payOS SDK requires httpx. Install it with: pip install httpx") from None
43
+
44
+ from .resources.v1.payouts import AsyncPayouts
45
+ from .resources.v1.payouts_account import AsyncPayoutsAccount
46
+ from .resources.v2.payment_requests import AsyncPaymentRequests
47
+ from .resources.webhooks import AsyncWebhooks
48
+
49
+ T = TypeVar("T")
50
+ ResponseT = TypeVar("ResponseT")
51
+ log: logging.Logger = logging.getLogger(__name__)
52
+ log.addFilter(SensitiveHeadersFilter())
53
+
54
+ DEFAULT_BASE_URL = "https://api-merchant.payos.vn"
55
+ DEFAULT_TIMEOUT = 60.0
56
+ DEFAULT_MAX_RETRIES = 2
57
+
58
+
59
+ class AsyncPayOS:
60
+ """Asynchronous payOS API client."""
61
+
62
+ client_id: str
63
+ api_key: str
64
+ checksum_key: str
65
+
66
+ def __init__(
67
+ self,
68
+ client_id: Optional[str] = None,
69
+ api_key: Optional[str] = None,
70
+ checksum_key: Optional[str] = None,
71
+ partner_code: Optional[str] = None,
72
+ *,
73
+ base_url: Optional[str] = None,
74
+ timeout: Optional[float] = None,
75
+ max_retries: Optional[int] = None,
76
+ http_client: Optional[httpx.AsyncClient] = None,
77
+ ) -> None:
78
+ """Initialize async payOS client.
79
+
80
+ Args:
81
+ client_id: payOS client ID. Defaults to PAYOS_CLIENT_ID env var.
82
+ api_key: payOS API key. Defaults to PAYOS_API_KEY env var.
83
+ checksum_key: payOS checksum key. Defaults to PAYOS_CHECKSUM_KEY env var.
84
+ partner_code: Optional partner code. Defaults to PAYOS_PARTNER_CODE env var.
85
+ base_url: API base URL. Defaults to PAYOS_BASE_URL env var or official URL.
86
+ timeout: Request timeout in seconds. Defaults to 60.
87
+ max_retries: Maximum number of retries. Defaults to 2.
88
+ http_client: Custom httpx.AsyncClient instance.
89
+ """
90
+ # Required credentials
91
+ if client_id is None:
92
+ client_id = get_env_var("PAYOS_CLIENT_ID")
93
+ if not client_id:
94
+ raise PayOSError(
95
+ "The PAYOS_CLIENT_ID environment variable is missing or empty; "
96
+ "either provide it, or instantiate the payOS client with a client_id option."
97
+ )
98
+ self.client_id = client_id
99
+
100
+ if api_key is None:
101
+ api_key = get_env_var("PAYOS_API_KEY")
102
+ if not api_key:
103
+ raise PayOSError(
104
+ "The PAYOS_API_KEY environment variable is missing or empty; "
105
+ "either provide it, or instantiate the payOS client with an api_key option."
106
+ )
107
+ self.api_key = api_key
108
+
109
+ if checksum_key is None:
110
+ checksum_key = get_env_var("PAYOS_CHECKSUM_KEY")
111
+ if not checksum_key:
112
+ raise PayOSError(
113
+ "The PAYOS_CHECKSUM_KEY environment variable is missing or empty; "
114
+ "either provide it, or instantiate the payOS client with a checksum_key option."
115
+ )
116
+ self.checksum_key = checksum_key
117
+
118
+ # Optional configuration
119
+ self.partner_code = partner_code or get_env_var("PAYOS_PARTNER_CODE")
120
+ self.base_url = base_url or get_env_var("PAYOS_BASE_URL") or DEFAULT_BASE_URL
121
+ self.timeout = timeout if timeout is not None else DEFAULT_TIMEOUT
122
+ self.max_retries = max_retries if max_retries is not None else DEFAULT_MAX_RETRIES
123
+
124
+ # Validate timeout
125
+ if self.timeout is not None:
126
+ validate_positive_number("timeout", self.timeout)
127
+
128
+ # Set up crypto provider
129
+ self.crypto = CryptoProvider()
130
+
131
+ # Set up HTTP client
132
+ self._http_client = http_client or httpx.AsyncClient()
133
+ self._own_http_client = http_client is None
134
+
135
+ @property
136
+ def user_agent(self) -> str:
137
+ return f"{self.__class__.__name__}/Python {__version__}"
138
+
139
+ async def aclose(self) -> None:
140
+ """Close the HTTP client if we own it."""
141
+ if self._own_http_client and self._http_client is not None:
142
+ await self._http_client.aclose()
143
+
144
+ async def __aenter__(self) -> "AsyncPayOS":
145
+ return self
146
+
147
+ async def __aexit__(
148
+ self,
149
+ exc_type: Optional[type[BaseException]],
150
+ exc_val: Optional[BaseException],
151
+ exc_tb: Optional[TracebackType],
152
+ ) -> None:
153
+ await self.aclose()
154
+
155
+ def _build_headers(self, additional_headers: Optional[dict[str, str]] = None) -> dict[str, str]:
156
+ """Build headers for API requests."""
157
+ headers = {
158
+ "x-client-id": self.client_id,
159
+ "x-api-key": self.api_key,
160
+ "Content-Type": "application/json",
161
+ "User-Agent": self.user_agent,
162
+ }
163
+
164
+ if self.partner_code:
165
+ headers["x-partner-code"] = self.partner_code
166
+
167
+ if additional_headers:
168
+ headers.update(additional_headers)
169
+
170
+ return headers
171
+
172
+ def _build_url(
173
+ self, path: str, query: Optional[dict[str, Any]] = None, url: Optional[str] = None
174
+ ) -> str:
175
+ """Build full URL from path and query parameters."""
176
+ url = urljoin(self.base_url, path.lstrip("/"))
177
+
178
+ if query:
179
+ # Filter out None values and convert to strings
180
+ filtered_query = {}
181
+ for key, value in query.items():
182
+ if value is not None:
183
+ if isinstance(value, (dict, list)):
184
+ filtered_query[key] = json.dumps(value)
185
+ else:
186
+ filtered_query[key] = str(value)
187
+
188
+ if filtered_query:
189
+ url += "?" + urlencode(filtered_query)
190
+
191
+ return url
192
+
193
+ def _build_body(self, body: Any) -> Optional[Union[str, bytes]]:
194
+ """Build request body."""
195
+ if body is None:
196
+ return None
197
+
198
+ if isinstance(body, (str, bytes)):
199
+ return body
200
+
201
+ try:
202
+ from ._core.models import PayOSBaseModel
203
+
204
+ if isinstance(body, PayOSBaseModel):
205
+ if hasattr(body, "model_dump_camel_case"):
206
+ body = body.model_dump_camel_case()
207
+ else:
208
+ body = body.model_dump(by_alias=True)
209
+ except ImportError:
210
+ pass
211
+
212
+ if isinstance(body, dict):
213
+ return json.dumps(body)
214
+
215
+ try:
216
+ return json.dumps(body)
217
+ except TypeError:
218
+ return str(body)
219
+
220
+ def _build_request(self, options: FinalRequestOptions) -> httpx.Request:
221
+ url = self._build_url(options.path or "", options.query, options.url)
222
+ body = options.body
223
+ if body is not None:
224
+ body = self._handle_signature(options, body)
225
+ headers = self._build_headers(options.headers)
226
+ request_body = self._build_body(body)
227
+
228
+ return self._http_client.build_request(
229
+ url=url,
230
+ method=options.method,
231
+ headers=headers,
232
+ content=request_body,
233
+ timeout=self.timeout if not options.timeout else options.timeout,
234
+ )
235
+
236
+ def _should_retry(self, response: httpx.Response, retry_count: int) -> bool:
237
+ """Determine if request should be retried."""
238
+ if retry_count >= self.max_retries:
239
+ return False
240
+
241
+ if response.status_code in [408, 429] or response.status_code >= 500:
242
+ return True
243
+
244
+ return False
245
+
246
+ def _calculate_retry_delay(self, retry_count: int, response_headers: httpx.Headers) -> float:
247
+ """Calculate delay before retry."""
248
+
249
+ # Check for Retry-After header
250
+ # About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
251
+ timeout_ms: Union[float, None] = None
252
+ retry_after = response_headers.get("retry-after")
253
+ rate_limit_reset = response_headers.get("x-ratelimit-reset")
254
+ if retry_after:
255
+ try:
256
+ timeout_ms = float(retry_after)
257
+ except ValueError:
258
+ pass
259
+
260
+ retry_date_tuple = email.utils.parsedate_tz(retry_after)
261
+ if retry_date_tuple is not None:
262
+ retry_date = email.utils.mktime_tz(retry_date_tuple)
263
+ timeout_ms = float(retry_date) - time.time()
264
+
265
+ if rate_limit_reset:
266
+ timeout_ms = float(rate_limit_reset) - time.time()
267
+
268
+ if timeout_ms is not None and 0 < timeout_ms <= 60:
269
+ return timeout_ms
270
+
271
+ # Exponential backoff with jitter
272
+ base_delay = 0.5
273
+ max_delay = 10.0
274
+ delay: float = min(base_delay * (2**retry_count), max_delay)
275
+
276
+ # Add jitter (random factor between 0.75 and 1.0)
277
+
278
+ jitter = 0.75 + random.random() * 0.25
279
+
280
+ return delay * jitter
281
+
282
+ def _handle_signature(self, options: FinalRequestOptions, body: Any) -> Any:
283
+ """Handle request signature generation."""
284
+ if not options.signature_request or not body:
285
+ return body
286
+
287
+ original_body = body
288
+ try:
289
+ from ._core.models import PayOSBaseModel
290
+
291
+ if isinstance(body, PayOSBaseModel):
292
+ if hasattr(body, "model_dump_camel_case"):
293
+ body_dict = body.model_dump_camel_case()
294
+ else:
295
+ body_dict = body.model_dump(by_alias=True)
296
+ elif isinstance(body, str):
297
+ try:
298
+ body_dict = json.loads(body)
299
+ except json.JSONDecodeError:
300
+ raise InvalidSignatureError(
301
+ "Invalid JSON body for signature generation"
302
+ ) from None
303
+ else:
304
+ body_dict = body
305
+ except ImportError:
306
+ if isinstance(body, str):
307
+ try:
308
+ body_dict = json.loads(body)
309
+ except json.JSONDecodeError:
310
+ raise InvalidSignatureError(
311
+ "Invalid JSON body for signature generation"
312
+ ) from None
313
+ else:
314
+ body_dict = body
315
+
316
+ if not isinstance(body_dict, dict):
317
+ raise InvalidSignatureError("Body must be a dictionary for signature generation")
318
+
319
+ signature = None
320
+ if options.signature_request == "create-payment-link":
321
+ signature = self.crypto.create_signature_of_payment_request(
322
+ body_dict,
323
+ self.checksum_key,
324
+ )
325
+ elif options.signature_request == "body":
326
+ signature = self.crypto.create_signature_from_object(body_dict, self.checksum_key)
327
+ elif options.signature_request == "header":
328
+ signature = self.crypto.create_signature(self.checksum_key, body_dict)
329
+
330
+ if not signature:
331
+ raise InvalidSignatureError("Failed to generate signature")
332
+
333
+ if options.signature_request in ["create-payment-link", "body"]:
334
+ body_dict["signature"] = signature
335
+ return body_dict
336
+ else: # header
337
+ options.headers = options.headers or {}
338
+ options.headers["x-signature"] = signature
339
+ return original_body
340
+
341
+ def _verify_response_signature(
342
+ self, options: FinalRequestOptions, response_data: Any, response_signature: Optional[str]
343
+ ) -> None:
344
+ """Verify response signature."""
345
+ if not options.signature_response or not response_data:
346
+ return
347
+
348
+ if not response_signature:
349
+ raise InvalidSignatureError("Response signature missing")
350
+
351
+ if options.signature_response == "body":
352
+ expected_signature = self.crypto.create_signature_from_object(
353
+ response_data,
354
+ self.checksum_key,
355
+ )
356
+ elif options.signature_response == "header":
357
+ expected_signature = self.crypto.create_signature(self.checksum_key, response_data)
358
+ else:
359
+ raise InvalidSignatureError("Invalid signature response type")
360
+
361
+ if not expected_signature or response_signature != expected_signature:
362
+ raise InvalidSignatureError("Response signature verification failed")
363
+
364
+ async def request(
365
+ self,
366
+ options: FinalRequestOptions,
367
+ *,
368
+ retry_count: int = 0,
369
+ cast_to: type[ResponseT],
370
+ ) -> ResponseT:
371
+ """Make HTTP request with retry logic."""
372
+ max_retries = options.max_retries if options.max_retries is not None else self.max_retries
373
+ request = self._build_request(options=options)
374
+ log.debug("Request options: %s", request_to_dict(request))
375
+
376
+ try:
377
+ response = await self._http_client.send(request=request)
378
+
379
+ if not response.is_success:
380
+ should_retry = self._should_retry(response, retry_count)
381
+
382
+ error_data = None
383
+ try:
384
+ error_json = response.json()
385
+ error_data = error_json
386
+ except Exception:
387
+ pass
388
+
389
+ if should_retry:
390
+ delay = self._calculate_retry_delay(retry_count, response.headers)
391
+ log.info("Retrying request to %s in %f seconds", request.url, delay)
392
+ await asyncio.sleep(delay)
393
+ return await self.request(
394
+ options,
395
+ cast_to=cast_to,
396
+ retry_count=retry_count + 1,
397
+ )
398
+
399
+ raise APIError.from_response(response, error_data=error_data) from None
400
+
401
+ log.debug("HTTP Response: %s", response_to_dict(response=response))
402
+
403
+ try:
404
+ response_json = response.json()
405
+ except Exception:
406
+ raise APIError(
407
+ f"Invalid JSON response: {response.text[:200]}",
408
+ status_code=response.status_code,
409
+ response=response,
410
+ ) from None
411
+
412
+ code = response_json.get("code")
413
+ desc = response_json.get("desc")
414
+ data = response_json.get("data")
415
+ signature = response_json.get("signature")
416
+
417
+ if code != "00":
418
+ raise APIError(
419
+ desc or "API error",
420
+ error_code=code,
421
+ error_desc=desc,
422
+ status_code=response.status_code,
423
+ response=response,
424
+ )
425
+
426
+ if options.signature_response == "body":
427
+ self._verify_response_signature(options, data, signature)
428
+ elif options.signature_response == "header":
429
+ header_signature = response.headers.get("x-signature")
430
+ self._verify_response_signature(options, data, header_signature)
431
+
432
+ return cast_response_to(cast_to, data)
433
+
434
+ except httpx.TimeoutException:
435
+ log.debug("Encountered httpx.TimeoutException", exc_info=True)
436
+ if retry_count < max_retries:
437
+ delay = self._calculate_retry_delay(retry_count, httpx.Headers())
438
+ log.info("Retrying request to %s in %f seconds", request.url, delay)
439
+ await asyncio.sleep(delay)
440
+ return await self.request(
441
+ options,
442
+ cast_to=cast_to,
443
+ retry_count=retry_count + 1,
444
+ )
445
+ raise ConnectionTimeoutError("Request timed out") from None
446
+
447
+ except httpx.ConnectError:
448
+ log.debug("Encountered Exception", exc_info=True)
449
+ if retry_count < max_retries:
450
+ delay = self._calculate_retry_delay(retry_count, httpx.Headers())
451
+ log.info("Retrying request to %s in %f seconds", request.url, delay)
452
+ await asyncio.sleep(delay)
453
+ return await self.request(
454
+ options,
455
+ cast_to=cast_to,
456
+ retry_count=retry_count + 1,
457
+ )
458
+ raise ConnectionError("Failed to connect") from None
459
+
460
+ except Exception:
461
+ raise
462
+
463
+ async def get(
464
+ self, path: str, *, cast_to: type[ResponseT], **kwargs: Unpack[RequestOptions]
465
+ ) -> ResponseT:
466
+ """Make GET request."""
467
+ options = FinalRequestOptions(method="GET", path=path, **kwargs)
468
+ return await self.request(options, cast_to=cast_to)
469
+
470
+ async def post(
471
+ self, path: str, *, cast_to: type[ResponseT], **kwargs: Unpack[RequestOptions]
472
+ ) -> ResponseT:
473
+ """Make POST request."""
474
+ options = FinalRequestOptions(method="POST", path=path, **kwargs)
475
+ return await self.request(options, cast_to=cast_to)
476
+
477
+ async def put(
478
+ self, path: str, *, cast_to: type[ResponseT], **kwargs: Unpack[RequestOptions]
479
+ ) -> ResponseT:
480
+ """Make PUT request."""
481
+ options = FinalRequestOptions(method="PUT", path=path, **kwargs)
482
+ return await self.request(options, cast_to=cast_to)
483
+
484
+ async def patch(
485
+ self, path: str, *, cast_to: type[ResponseT], **kwargs: Unpack[RequestOptions]
486
+ ) -> ResponseT:
487
+ """Make PATCH request."""
488
+ options = FinalRequestOptions(method="PATCH", path=path, **kwargs)
489
+ return await self.request(options, cast_to=cast_to)
490
+
491
+ async def delete(
492
+ self, path: str, *, cast_to: type[ResponseT], **kwargs: Unpack[RequestOptions]
493
+ ) -> ResponseT:
494
+ """Make DELETE request."""
495
+ options = FinalRequestOptions(method="DELETE", path=path, **kwargs)
496
+ return await self.request(options, cast_to=cast_to)
497
+
498
+ async def download(self, path: str, **kwargs: Unpack[RequestOptions]) -> FileDownloadResponse:
499
+ """Download a file from the API."""
500
+ from ._core import FileDownloadResponse
501
+
502
+ options = FinalRequestOptions(method="GET", path=path, **kwargs)
503
+ request = self._build_request(options)
504
+
505
+ try:
506
+ response = await self._http_client.send(request=request)
507
+ if not response.is_success:
508
+ error_data = None
509
+ try:
510
+ error_json = response.json()
511
+ error_data = error_json
512
+ except Exception:
513
+ pass
514
+ raise APIError.from_response(response, error_data=error_data)
515
+
516
+ filename = None
517
+ content_disposition = response.headers.get("content-disposition")
518
+ if content_disposition:
519
+ import re
520
+
521
+ match = re.search(r'filename="([^"]+)"', content_disposition)
522
+ if match:
523
+ filename = match.group(1)
524
+
525
+ if not filename:
526
+ from urllib.parse import urlparse
527
+
528
+ parsed_url = urlparse(str(request.url))
529
+ filename = parsed_url.path.split("/")[-1] if parsed_url.path else None
530
+
531
+ content_size = len(response.content)
532
+ return FileDownloadResponse(
533
+ data=response.content,
534
+ filename=filename,
535
+ content_type=response.headers.get("content-type"),
536
+ size=content_size,
537
+ headers=dict(response.headers),
538
+ )
539
+
540
+ except httpx.TimeoutException:
541
+ raise ConnectionTimeoutError("Download request timed out") from None
542
+
543
+ except httpx.ConnectError:
544
+ raise ConnectionError("Failed to connect for download") from None
545
+
546
+ except Exception:
547
+ raise
548
+
549
+ @cached_property
550
+ def payment_requests(self) -> AsyncPaymentRequests:
551
+ return AsyncPaymentRequests(self)
552
+
553
+ @cached_property
554
+ def webhooks(self) -> AsyncWebhooks:
555
+ return AsyncWebhooks(self)
556
+
557
+ @cached_property
558
+ def payouts(self) -> AsyncPayouts:
559
+ return AsyncPayouts(self)
560
+
561
+ @cached_property
562
+ def payouts_account(self) -> AsyncPayoutsAccount:
563
+ return AsyncPayoutsAccount(self)