airwallex-sdk 0.1.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.
- airwallex/__init__.py +74 -0
- airwallex/api/__init__.py +37 -0
- airwallex/api/account.py +107 -0
- airwallex/api/account_detail.py +469 -0
- airwallex/api/base.py +488 -0
- airwallex/api/beneficiary.py +156 -0
- airwallex/api/financial_transaction.py +123 -0
- airwallex/api/invoice.py +257 -0
- airwallex/api/issuing_authorization.py +313 -0
- airwallex/api/issuing_card.py +411 -0
- airwallex/api/issuing_cardholder.py +234 -0
- airwallex/api/issuing_config.py +80 -0
- airwallex/api/issuing_digital_wallet_token.py +249 -0
- airwallex/api/issuing_transaction.py +231 -0
- airwallex/api/issuing_transaction_dispute.py +339 -0
- airwallex/api/payment.py +148 -0
- airwallex/client.py +396 -0
- airwallex/exceptions.py +222 -0
- airwallex/models/__init__.py +69 -0
- airwallex/models/account.py +51 -0
- airwallex/models/account_detail.py +259 -0
- airwallex/models/base.py +121 -0
- airwallex/models/beneficiary.py +70 -0
- airwallex/models/financial_transaction.py +30 -0
- airwallex/models/fx.py +58 -0
- airwallex/models/invoice.py +102 -0
- airwallex/models/issuing_authorization.py +41 -0
- airwallex/models/issuing_card.py +135 -0
- airwallex/models/issuing_cardholder.py +52 -0
- airwallex/models/issuing_common.py +83 -0
- airwallex/models/issuing_config.py +62 -0
- airwallex/models/issuing_digital_wallet_token.py +38 -0
- airwallex/models/issuing_transaction.py +42 -0
- airwallex/models/issuing_transaction_dispute.py +59 -0
- airwallex/models/payment.py +81 -0
- airwallex/utils.py +107 -0
- airwallex_sdk-0.1.0.dist-info/METADATA +202 -0
- airwallex_sdk-0.1.0.dist-info/RECORD +39 -0
- airwallex_sdk-0.1.0.dist-info/WHEEL +4 -0
airwallex/api/base.py
ADDED
@@ -0,0 +1,488 @@
|
|
1
|
+
"""
|
2
|
+
Base API class for the Airwallex SDK.
|
3
|
+
"""
|
4
|
+
import asyncio
|
5
|
+
import logging
|
6
|
+
from typing import (
|
7
|
+
Any,
|
8
|
+
Dict,
|
9
|
+
List,
|
10
|
+
Optional,
|
11
|
+
Type,
|
12
|
+
TypeVar,
|
13
|
+
Union,
|
14
|
+
Coroutine,
|
15
|
+
Generator,
|
16
|
+
AsyncGenerator,
|
17
|
+
Generic,
|
18
|
+
cast,
|
19
|
+
get_args,
|
20
|
+
get_origin
|
21
|
+
)
|
22
|
+
import httpx
|
23
|
+
|
24
|
+
from ..models.base import AirwallexModel, PaginatedResponse
|
25
|
+
from ..utils import snake_to_pascal_case, serialize, deserialize
|
26
|
+
from ..exceptions import (
|
27
|
+
AirwallexAPIError,
|
28
|
+
AuthenticationError,
|
29
|
+
RateLimitError,
|
30
|
+
ResourceNotFoundError,
|
31
|
+
ValidationError,
|
32
|
+
ServerError
|
33
|
+
)
|
34
|
+
|
35
|
+
logger = logging.getLogger(__name__)
|
36
|
+
|
37
|
+
T = TypeVar("T", bound=AirwallexModel)
|
38
|
+
ClientType = TypeVar("ClientType")
|
39
|
+
|
40
|
+
class AirwallexAPIBase(Generic[T]):
|
41
|
+
"""
|
42
|
+
Base class for Airwallex API endpoints.
|
43
|
+
|
44
|
+
This class provides standard CRUD methods and pagination handling
|
45
|
+
for all API endpoints. It serves as the foundation for specific
|
46
|
+
API endpoint implementations.
|
47
|
+
"""
|
48
|
+
endpoint: str = ""
|
49
|
+
model_class: Type[T] = cast(Type[T], AirwallexModel) # Will be overridden by subclasses
|
50
|
+
|
51
|
+
def __init__(
|
52
|
+
self,
|
53
|
+
*,
|
54
|
+
client: Any,
|
55
|
+
data: Optional[Dict[str, Any]] = None,
|
56
|
+
parent: Optional["AirwallexAPIBase"] = None,
|
57
|
+
parent_path: Optional[str] = None # e.g. "/api/v1/accounts/{account_id}"
|
58
|
+
) -> None:
|
59
|
+
self.client = client
|
60
|
+
self.data: Dict[str, Any] = data or {}
|
61
|
+
self.parent: Optional["AirwallexAPIBase"] = parent
|
62
|
+
self.parent_path: Optional[str] = parent_path
|
63
|
+
|
64
|
+
def __getattr__(self, item: str) -> Any:
|
65
|
+
# If the attribute exists in the model's data, return it.
|
66
|
+
if item in self.data:
|
67
|
+
return self.data[item]
|
68
|
+
|
69
|
+
# If the model has an ID, we can try to access a subresource
|
70
|
+
if not getattr(self, 'id', None):
|
71
|
+
raise AttributeError(f"No such attribute '{item}' in {self.__class__.__name__} context.")
|
72
|
+
|
73
|
+
# Try to load an API module for this attribute.
|
74
|
+
try:
|
75
|
+
from importlib import import_module
|
76
|
+
base_package = self.client.__class__.__module__.split(".")[0]
|
77
|
+
module = import_module(f"{base_package}.api.{item.lower()}")
|
78
|
+
# We define modules in pascal case, but refer to them as attributes in snake case.
|
79
|
+
api_class = getattr(module, snake_to_pascal_case(item))
|
80
|
+
return api_class(client=self.client, parent=self, parent_path=self._build_url(self.id))
|
81
|
+
except (ModuleNotFoundError, AttributeError):
|
82
|
+
# Split snake case item into a path e.g. report_details -> report/details
|
83
|
+
path_item = "/".join(item.split("_"))
|
84
|
+
|
85
|
+
# If no module exists for this attribute and model has an id, then assume the attribute
|
86
|
+
# is a valid endpoint suffix. Return a callable that makes a GET request.
|
87
|
+
def dynamic_endpoint(*args, **kwargs):
|
88
|
+
"""
|
89
|
+
:param dataframe: If True, return a DataFrame instead of a list of dictionaries.
|
90
|
+
"""
|
91
|
+
url = self._build_url(resource_id=self.id, suffix=path_item)
|
92
|
+
if not self.client.__class__.__name__.startswith('Async'):
|
93
|
+
response = self.client._request("GET", url, params=kwargs)
|
94
|
+
data = self._parse_response_data(response.json())
|
95
|
+
return data
|
96
|
+
else:
|
97
|
+
async def async_endpoint():
|
98
|
+
response = await self.client._request("GET", url, params=kwargs)
|
99
|
+
data = self._parse_response_data(response.json())
|
100
|
+
return data
|
101
|
+
return async_endpoint()
|
102
|
+
return dynamic_endpoint
|
103
|
+
|
104
|
+
def __repr__(self) -> str:
|
105
|
+
identifier = self.data.get("id", "unknown")
|
106
|
+
return f"<{self.__class__.__name__} id={identifier}>"
|
107
|
+
|
108
|
+
def __call__(self, resource_id: Optional[Any] = None, **kwargs: Any) -> Union[
|
109
|
+
T,
|
110
|
+
Generator[T, None, None],
|
111
|
+
Coroutine[Any, Any, AsyncGenerator[T, None]]
|
112
|
+
]:
|
113
|
+
"""
|
114
|
+
If a resource_id is provided, fetch and return a single instance;
|
115
|
+
otherwise, return a generator that yields resources one by one.
|
116
|
+
|
117
|
+
For sync clients, returns a Generator[T, None, None].
|
118
|
+
For async clients, returns a coroutine that yields an AsyncGenerator[T, None].
|
119
|
+
"""
|
120
|
+
if resource_id is not None:
|
121
|
+
if not self.client.__class__.__name__.startswith('Async'):
|
122
|
+
return self.fetch(resource_id)
|
123
|
+
else:
|
124
|
+
return self.fetch_async(resource_id)
|
125
|
+
else:
|
126
|
+
if not self.client.__class__.__name__.startswith('Async'):
|
127
|
+
return self.paginate_generator(**kwargs)
|
128
|
+
else:
|
129
|
+
return self.paginate_async_generator(**kwargs)
|
130
|
+
|
131
|
+
@classmethod
|
132
|
+
def get_endpoint(cls) -> str:
|
133
|
+
"""Get the API endpoint path."""
|
134
|
+
return cls.endpoint if cls.endpoint else cls.__name__.lower()
|
135
|
+
|
136
|
+
@staticmethod
|
137
|
+
def _parse_response_data(
|
138
|
+
response: Union[List[Any], Dict[str, Any]]
|
139
|
+
) -> List[Dict[str, Any]]:
|
140
|
+
"""Parse response data into a list of dictionaries."""
|
141
|
+
# If response is a dictionary with an 'items' key, it's paginated
|
142
|
+
if isinstance(response, dict) and 'items' in response:
|
143
|
+
return response['items']
|
144
|
+
# If response is a dictionary, wrap it in a list
|
145
|
+
if isinstance(response, dict):
|
146
|
+
return [response]
|
147
|
+
# If response is already a list, return it
|
148
|
+
return response
|
149
|
+
|
150
|
+
@property
|
151
|
+
def base_path(self) -> str:
|
152
|
+
"""Get the base API path for this endpoint."""
|
153
|
+
if self.parent_path:
|
154
|
+
return f"{self.parent_path}/{self.get_endpoint()}"
|
155
|
+
return f"/api/v1/{self.get_endpoint()}"
|
156
|
+
|
157
|
+
def _build_url(self, resource_id: Optional[Any] = None, suffix: str = "") -> str:
|
158
|
+
"""Build a URL for a specific resource."""
|
159
|
+
url = self.base_path
|
160
|
+
if resource_id is not None:
|
161
|
+
url = f"{url}/{resource_id}"
|
162
|
+
if suffix:
|
163
|
+
url = f"{url}/{suffix}"
|
164
|
+
return url
|
165
|
+
|
166
|
+
def show(self, indent: int = 0, indent_step: int = 2) -> str:
|
167
|
+
"""
|
168
|
+
Return a nicely formatted string representation of this model and its data.
|
169
|
+
"""
|
170
|
+
pad = " " * indent
|
171
|
+
lines = [f"{pad}{self.__class__.__name__}:"]
|
172
|
+
for key, value in self.data.items():
|
173
|
+
if isinstance(value, AirwallexAPIBase):
|
174
|
+
lines.append(f"{pad}{' ' * indent_step}{key}:")
|
175
|
+
lines.append(value.show(indent + indent_step, indent_step))
|
176
|
+
elif isinstance(value, list):
|
177
|
+
lines.append(f"{pad}{' ' * indent_step}{key}: [")
|
178
|
+
for item in value:
|
179
|
+
if isinstance(item, AirwallexAPIBase):
|
180
|
+
lines.append(item.show(indent + indent_step, indent_step))
|
181
|
+
else:
|
182
|
+
lines.append(f"{pad}{' ' * (indent_step * 2)}{item}")
|
183
|
+
lines.append(f"{pad}{' ' * indent_step}]")
|
184
|
+
else:
|
185
|
+
lines.append(f"{pad}{' ' * indent_step}{key}: {value}")
|
186
|
+
return "\n".join(lines)
|
187
|
+
|
188
|
+
def to_model(self) -> T:
|
189
|
+
"""Convert the raw data to a Pydantic model."""
|
190
|
+
if not self.data:
|
191
|
+
raise ValueError("No data available to convert to a model")
|
192
|
+
return self.model_class.from_api_response(self.data)
|
193
|
+
|
194
|
+
# Synchronous API methods
|
195
|
+
|
196
|
+
def fetch(self, resource_id: Any) -> T:
|
197
|
+
"""Fetch a single resource by ID."""
|
198
|
+
if self.client.__class__.__name__.startswith('Async'):
|
199
|
+
raise ValueError("This method requires a sync client.")
|
200
|
+
url = self._build_url(resource_id)
|
201
|
+
response = self.client._request("GET", url)
|
202
|
+
data = self._parse_response_data(response.json())
|
203
|
+
# If the returned data is a list, take the first item.
|
204
|
+
if isinstance(data, list):
|
205
|
+
data = data[0] if data else {}
|
206
|
+
return self.model_class.from_api_response(data)
|
207
|
+
|
208
|
+
def list(self, **params: Any) -> List[T]:
|
209
|
+
"""List resources with optional filtering parameters."""
|
210
|
+
if self.client.__class__.__name__.startswith('Async'):
|
211
|
+
raise ValueError("This method requires a sync client.")
|
212
|
+
url = self._build_url()
|
213
|
+
response = self.client._request("GET", url, params=serialize(params))
|
214
|
+
data_list = self._parse_response_data(response.json())
|
215
|
+
return [self.model_class.from_api_response(item) for item in data_list]
|
216
|
+
|
217
|
+
def create(self, payload: Union[Dict[str, Any], T]) -> T:
|
218
|
+
"""Create a new resource."""
|
219
|
+
if str(self.client.__class__.__name__).startswith('Async'):
|
220
|
+
raise ValueError("This method requires a sync client.")
|
221
|
+
|
222
|
+
# Convert Pydantic model to dict if needed
|
223
|
+
if isinstance(payload, AirwallexModel):
|
224
|
+
payload_dict = payload.to_api_dict()
|
225
|
+
else:
|
226
|
+
payload_dict = serialize(payload)
|
227
|
+
|
228
|
+
url = self._build_url()
|
229
|
+
response = self.client._request("POST", url, json=payload_dict)
|
230
|
+
data = self._parse_response_data(response.json())
|
231
|
+
# If the returned data is a list, take the first item.
|
232
|
+
if isinstance(data, list):
|
233
|
+
data = data[0] if data else {}
|
234
|
+
return self.model_class.from_api_response(data)
|
235
|
+
|
236
|
+
def update(self, resource_id: Any, payload: Union[Dict[str, Any], T]) -> T:
|
237
|
+
"""Update an existing resource."""
|
238
|
+
if self.client.__class__.__name__.startswith('Async'):
|
239
|
+
raise ValueError("This method requires a sync client.")
|
240
|
+
|
241
|
+
# Convert Pydantic model to dict if needed
|
242
|
+
if isinstance(payload, AirwallexModel):
|
243
|
+
payload_dict = payload.to_api_dict()
|
244
|
+
else:
|
245
|
+
payload_dict = serialize(payload)
|
246
|
+
|
247
|
+
url = self._build_url(resource_id)
|
248
|
+
response = self.client._request("PUT", url, json=payload_dict)
|
249
|
+
data = self._parse_response_data(response.json())
|
250
|
+
# If the returned data is a list, take the first item.
|
251
|
+
if isinstance(data, list):
|
252
|
+
data = data[0] if data else {}
|
253
|
+
return self.model_class.from_api_response(data)
|
254
|
+
|
255
|
+
def delete(self, resource_id: Any) -> None:
|
256
|
+
"""Delete a resource."""
|
257
|
+
if self.client.__class__.__name__.startswith('Async'):
|
258
|
+
raise ValueError("This method requires a sync client.")
|
259
|
+
url = self._build_url(resource_id)
|
260
|
+
self.client._request("DELETE", url)
|
261
|
+
|
262
|
+
def paginate(self, **params: Any) -> List[T]:
|
263
|
+
"""Fetch all pages of data."""
|
264
|
+
if str(self.client.__class__.__name__).startswith('Async'):
|
265
|
+
raise ValueError("This method requires a sync client.")
|
266
|
+
|
267
|
+
all_items: List[Dict[str, Any]] = []
|
268
|
+
page = params.get("page", 1)
|
269
|
+
page_size = params.get("page_size", 100)
|
270
|
+
|
271
|
+
while True:
|
272
|
+
params["page"] = page
|
273
|
+
params["page_size"] = page_size
|
274
|
+
url = self._build_url()
|
275
|
+
response = self.client._request("GET", url, params=serialize(params))
|
276
|
+
response_data = response.json()
|
277
|
+
|
278
|
+
# Check if response is paginated
|
279
|
+
if isinstance(response_data, dict) and 'items' in response_data:
|
280
|
+
items = response_data['items']
|
281
|
+
total_pages = response_data.get('total_pages', 1)
|
282
|
+
|
283
|
+
if not items:
|
284
|
+
break
|
285
|
+
|
286
|
+
all_items.extend(items)
|
287
|
+
|
288
|
+
if page >= total_pages:
|
289
|
+
break
|
290
|
+
|
291
|
+
page += 1
|
292
|
+
else:
|
293
|
+
# Not paginated, just use the data as is
|
294
|
+
page_data = self._parse_response_data(response_data)
|
295
|
+
if not page_data:
|
296
|
+
break
|
297
|
+
all_items.extend(page_data)
|
298
|
+
break
|
299
|
+
|
300
|
+
return [self.model_class.from_api_response(item) for item in all_items]
|
301
|
+
|
302
|
+
def paginate_generator(self, **params: Any) -> Generator[T, None, None]:
|
303
|
+
"""Generate items one by one from paginated results."""
|
304
|
+
if self.client.__class__.__name__.startswith('Async'):
|
305
|
+
raise ValueError("This method requires a sync client.")
|
306
|
+
|
307
|
+
page = params.get("page", 1)
|
308
|
+
page_size = params.get("page_size", 100)
|
309
|
+
|
310
|
+
while True:
|
311
|
+
params["page"] = page
|
312
|
+
params["page_size"] = page_size
|
313
|
+
url = self._build_url()
|
314
|
+
response = self.client._request("GET", url, params=serialize(params))
|
315
|
+
response_data = response.json()
|
316
|
+
|
317
|
+
# Check if response is paginated
|
318
|
+
if isinstance(response_data, dict) and 'items' in response_data:
|
319
|
+
items = response_data['items']
|
320
|
+
total_pages = response_data.get('total_pages', 1)
|
321
|
+
|
322
|
+
if not items:
|
323
|
+
break
|
324
|
+
|
325
|
+
for item in items:
|
326
|
+
yield self.model_class.from_api_response(item)
|
327
|
+
|
328
|
+
if page >= total_pages:
|
329
|
+
break
|
330
|
+
|
331
|
+
page += 1
|
332
|
+
else:
|
333
|
+
# Not paginated, just use the data as is
|
334
|
+
page_data = self._parse_response_data(response_data)
|
335
|
+
if not page_data:
|
336
|
+
break
|
337
|
+
|
338
|
+
for item in page_data:
|
339
|
+
yield self.model_class.from_api_response(item)
|
340
|
+
break
|
341
|
+
|
342
|
+
# Asynchronous API methods
|
343
|
+
|
344
|
+
async def fetch_async(self, resource_id: Any) -> T:
|
345
|
+
"""Fetch a single resource by ID asynchronously."""
|
346
|
+
if not self.client.__class__.__name__.startswith('Async'):
|
347
|
+
raise ValueError("This method requires an async client.")
|
348
|
+
url = self._build_url(resource_id)
|
349
|
+
response = await self.client._request("GET", url)
|
350
|
+
data = self._parse_response_data(response.json())
|
351
|
+
# If the returned data is a list, take the first item.
|
352
|
+
if isinstance(data, list):
|
353
|
+
data = data[0] if data else {}
|
354
|
+
return self.model_class.from_api_response(data)
|
355
|
+
|
356
|
+
async def list_async(self, **params: Any) -> List[T]:
|
357
|
+
"""List resources with optional filtering parameters asynchronously."""
|
358
|
+
if not self.client.__class__.__name__.startswith('Async'):
|
359
|
+
raise ValueError("This method requires an async client.")
|
360
|
+
url = self._build_url()
|
361
|
+
response = await self.client._request("GET", url, params=serialize(params))
|
362
|
+
data_list = self._parse_response_data(response.json())
|
363
|
+
return [self.model_class.from_api_response(item) for item in data_list]
|
364
|
+
|
365
|
+
async def create_async(self, payload: Union[Dict[str, Any], T]) -> T:
|
366
|
+
"""Create a new resource asynchronously."""
|
367
|
+
if not self.client.__class__.__name__.startswith('Async'):
|
368
|
+
raise ValueError("This method requires an async client.")
|
369
|
+
|
370
|
+
# Convert Pydantic model to dict if needed
|
371
|
+
if isinstance(payload, AirwallexModel):
|
372
|
+
payload_dict = payload.to_api_dict()
|
373
|
+
else:
|
374
|
+
payload_dict = serialize(payload)
|
375
|
+
|
376
|
+
url = self._build_url()
|
377
|
+
response = await self.client._request("POST", url, json=payload_dict)
|
378
|
+
data = self._parse_response_data(response.json())
|
379
|
+
# If the returned data is a list, take the first item.
|
380
|
+
if isinstance(data, list):
|
381
|
+
data = data[0] if data else {}
|
382
|
+
return self.model_class.from_api_response(data)
|
383
|
+
|
384
|
+
async def update_async(self, resource_id: Any, payload: Union[Dict[str, Any], T]) -> T:
|
385
|
+
"""Update an existing resource asynchronously."""
|
386
|
+
if not self.client.__class__.__name__.startswith('Async'):
|
387
|
+
raise ValueError("This method requires an async client.")
|
388
|
+
|
389
|
+
# Convert Pydantic model to dict if needed
|
390
|
+
if isinstance(payload, AirwallexModel):
|
391
|
+
payload_dict = payload.to_api_dict()
|
392
|
+
else:
|
393
|
+
payload_dict = serialize(payload)
|
394
|
+
|
395
|
+
url = self._build_url(resource_id)
|
396
|
+
response = await self.client._request("PUT", url, json=payload_dict)
|
397
|
+
data = self._parse_response_data(response.json())
|
398
|
+
# If the returned data is a list, take the first item.
|
399
|
+
if isinstance(data, list):
|
400
|
+
data = data[0] if data else {}
|
401
|
+
return self.model_class.from_api_response(data)
|
402
|
+
|
403
|
+
async def delete_async(self, resource_id: Any) -> None:
|
404
|
+
"""Delete a resource asynchronously."""
|
405
|
+
if not self.client.__class__.__name__.startswith('Async'):
|
406
|
+
raise ValueError("This method requires an async client.")
|
407
|
+
url = self._build_url(resource_id)
|
408
|
+
await self.client._request("DELETE", url)
|
409
|
+
|
410
|
+
async def paginate_async(self, **params: Any) -> List[T]:
|
411
|
+
"""Fetch all pages of data asynchronously."""
|
412
|
+
if not self.client.__class__.__name__.startswith('Async'):
|
413
|
+
raise ValueError("This method requires an async client.")
|
414
|
+
|
415
|
+
all_items: List[Dict[str, Any]] = []
|
416
|
+
page = params.get("page", 1)
|
417
|
+
page_size = params.get("page_size", 100)
|
418
|
+
|
419
|
+
while True:
|
420
|
+
params["page"] = page
|
421
|
+
params["page_size"] = page_size
|
422
|
+
url = self._build_url()
|
423
|
+
response = await self.client._request("GET", url, params=serialize(params))
|
424
|
+
response_data = response.json()
|
425
|
+
|
426
|
+
# Check if response is paginated
|
427
|
+
if isinstance(response_data, dict) and 'items' in response_data:
|
428
|
+
items = response_data['items']
|
429
|
+
total_pages = response_data.get('total_pages', 1)
|
430
|
+
|
431
|
+
if not items:
|
432
|
+
break
|
433
|
+
|
434
|
+
all_items.extend(items)
|
435
|
+
|
436
|
+
if page >= total_pages:
|
437
|
+
break
|
438
|
+
|
439
|
+
page += 1
|
440
|
+
else:
|
441
|
+
# Not paginated, just use the data as is
|
442
|
+
page_data = self._parse_response_data(response_data)
|
443
|
+
if not page_data:
|
444
|
+
break
|
445
|
+
all_items.extend(page_data)
|
446
|
+
break
|
447
|
+
|
448
|
+
return [self.model_class.from_api_response(item) for item in all_items]
|
449
|
+
|
450
|
+
async def paginate_async_generator(self, **params: Any) -> AsyncGenerator[T, None]:
|
451
|
+
"""Generate items one by one from paginated results asynchronously."""
|
452
|
+
if not self.client.__class__.__name__.startswith('Async'):
|
453
|
+
raise ValueError("This method requires an async client.")
|
454
|
+
|
455
|
+
page = params.get("page", 1)
|
456
|
+
page_size = params.get("page_size", 100)
|
457
|
+
|
458
|
+
while True:
|
459
|
+
params["page"] = page
|
460
|
+
params["page_size"] = page_size
|
461
|
+
url = self._build_url()
|
462
|
+
response = await self.client._request("GET", url, params=serialize(params))
|
463
|
+
response_data = response.json()
|
464
|
+
|
465
|
+
# Check if response is paginated
|
466
|
+
if isinstance(response_data, dict) and 'items' in response_data:
|
467
|
+
items = response_data['items']
|
468
|
+
total_pages = response_data.get('total_pages', 1)
|
469
|
+
|
470
|
+
if not items:
|
471
|
+
break
|
472
|
+
|
473
|
+
for item in items:
|
474
|
+
yield self.model_class.from_api_response(item)
|
475
|
+
|
476
|
+
if page >= total_pages:
|
477
|
+
break
|
478
|
+
|
479
|
+
page += 1
|
480
|
+
else:
|
481
|
+
# Not paginated, just use the data as is
|
482
|
+
page_data = self._parse_response_data(response_data)
|
483
|
+
if not page_data:
|
484
|
+
break
|
485
|
+
|
486
|
+
for item in page_data:
|
487
|
+
yield self.model_class.from_api_response(item)
|
488
|
+
break
|
@@ -0,0 +1,156 @@
|
|
1
|
+
"""
|
2
|
+
Airwallex Beneficiary API.
|
3
|
+
"""
|
4
|
+
from typing import Dict, Any, List, Optional, Type, TypeVar, Union, cast
|
5
|
+
from ..models.beneficiary import Beneficiary, BeneficiaryCreateRequest, BeneficiaryUpdateRequest
|
6
|
+
from .base import AirwallexAPIBase
|
7
|
+
|
8
|
+
T = TypeVar("T", bound=Beneficiary)
|
9
|
+
|
10
|
+
|
11
|
+
class Beneficiary(AirwallexAPIBase[Beneficiary]):
|
12
|
+
"""
|
13
|
+
Operations for Airwallex beneficiaries.
|
14
|
+
|
15
|
+
Beneficiaries represent recipients of payments.
|
16
|
+
"""
|
17
|
+
endpoint = "beneficiaries"
|
18
|
+
model_class = cast(Type[Beneficiary], Beneficiary)
|
19
|
+
|
20
|
+
def create_from_model(self, beneficiary: BeneficiaryCreateRequest) -> Beneficiary:
|
21
|
+
"""
|
22
|
+
Create a new beneficiary using a Pydantic model.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
beneficiary: BeneficiaryCreateRequest model with beneficiary creation details.
|
26
|
+
|
27
|
+
Returns:
|
28
|
+
Beneficiary: The created beneficiary.
|
29
|
+
"""
|
30
|
+
return self.create(beneficiary)
|
31
|
+
|
32
|
+
async def create_from_model_async(self, beneficiary: BeneficiaryCreateRequest) -> Beneficiary:
|
33
|
+
"""
|
34
|
+
Create a new beneficiary using a Pydantic model asynchronously.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
beneficiary: BeneficiaryCreateRequest model with beneficiary creation details.
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
Beneficiary: The created beneficiary.
|
41
|
+
"""
|
42
|
+
return await self.create_async(beneficiary)
|
43
|
+
|
44
|
+
def update_from_model(self, beneficiary_id: str, beneficiary: BeneficiaryUpdateRequest) -> Beneficiary:
|
45
|
+
"""
|
46
|
+
Update a beneficiary using a Pydantic model.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
beneficiary_id: The ID of the beneficiary to update.
|
50
|
+
beneficiary: BeneficiaryUpdateRequest model with beneficiary update details.
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
Beneficiary: The updated beneficiary.
|
54
|
+
"""
|
55
|
+
return self.update(beneficiary_id, beneficiary)
|
56
|
+
|
57
|
+
async def update_from_model_async(self, beneficiary_id: str, beneficiary: BeneficiaryUpdateRequest) -> Beneficiary:
|
58
|
+
"""
|
59
|
+
Update a beneficiary using a Pydantic model asynchronously.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
beneficiary_id: The ID of the beneficiary to update.
|
63
|
+
beneficiary: BeneficiaryUpdateRequest model with beneficiary update details.
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
Beneficiary: The updated beneficiary.
|
67
|
+
"""
|
68
|
+
return await self.update_async(beneficiary_id, beneficiary)
|
69
|
+
|
70
|
+
def validate(self, beneficiary: BeneficiaryCreateRequest) -> Dict[str, Any]:
|
71
|
+
"""
|
72
|
+
Validate a beneficiary without creating it.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
beneficiary: BeneficiaryCreateRequest model with beneficiary details.
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
Dict[str, Any]: Validation results.
|
79
|
+
"""
|
80
|
+
url = self._build_url(suffix="validate")
|
81
|
+
|
82
|
+
if not self.client.__class__.__name__.startswith('Async'):
|
83
|
+
response = self.client._request("POST", url, json=beneficiary.to_api_dict())
|
84
|
+
return response.json()
|
85
|
+
else:
|
86
|
+
raise ValueError("Use validate_async for async clients")
|
87
|
+
|
88
|
+
async def validate_async(self, beneficiary: BeneficiaryCreateRequest) -> Dict[str, Any]:
|
89
|
+
"""
|
90
|
+
Validate a beneficiary without creating it asynchronously.
|
91
|
+
|
92
|
+
Args:
|
93
|
+
beneficiary: BeneficiaryCreateRequest model with beneficiary details.
|
94
|
+
|
95
|
+
Returns:
|
96
|
+
Dict[str, Any]: Validation results.
|
97
|
+
"""
|
98
|
+
url = self._build_url(suffix="validate")
|
99
|
+
|
100
|
+
if self.client.__class__.__name__.startswith('Async'):
|
101
|
+
response = await self.client._request("POST", url, json=beneficiary.to_api_dict())
|
102
|
+
return response.json()
|
103
|
+
else:
|
104
|
+
raise ValueError("Use validate for sync clients")
|
105
|
+
|
106
|
+
def deactivate(self, beneficiary_id: str) -> Beneficiary:
|
107
|
+
"""
|
108
|
+
Deactivate a beneficiary.
|
109
|
+
|
110
|
+
Args:
|
111
|
+
beneficiary_id: The ID of the beneficiary to deactivate.
|
112
|
+
|
113
|
+
Returns:
|
114
|
+
Beneficiary: The deactivated beneficiary.
|
115
|
+
"""
|
116
|
+
update_request = BeneficiaryUpdateRequest(status="disabled")
|
117
|
+
return self.update(beneficiary_id, update_request)
|
118
|
+
|
119
|
+
async def deactivate_async(self, beneficiary_id: str) -> Beneficiary:
|
120
|
+
"""
|
121
|
+
Deactivate a beneficiary asynchronously.
|
122
|
+
|
123
|
+
Args:
|
124
|
+
beneficiary_id: The ID of the beneficiary to deactivate.
|
125
|
+
|
126
|
+
Returns:
|
127
|
+
Beneficiary: The deactivated beneficiary.
|
128
|
+
"""
|
129
|
+
update_request = BeneficiaryUpdateRequest(status="disabled")
|
130
|
+
return await self.update_async(beneficiary_id, update_request)
|
131
|
+
|
132
|
+
def activate(self, beneficiary_id: str) -> Beneficiary:
|
133
|
+
"""
|
134
|
+
Activate a beneficiary.
|
135
|
+
|
136
|
+
Args:
|
137
|
+
beneficiary_id: The ID of the beneficiary to activate.
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
Beneficiary: The activated beneficiary.
|
141
|
+
"""
|
142
|
+
update_request = BeneficiaryUpdateRequest(status="active")
|
143
|
+
return self.update(beneficiary_id, update_request)
|
144
|
+
|
145
|
+
async def activate_async(self, beneficiary_id: str) -> Beneficiary:
|
146
|
+
"""
|
147
|
+
Activate a beneficiary asynchronously.
|
148
|
+
|
149
|
+
Args:
|
150
|
+
beneficiary_id: The ID of the beneficiary to activate.
|
151
|
+
|
152
|
+
Returns:
|
153
|
+
Beneficiary: The activated beneficiary.
|
154
|
+
"""
|
155
|
+
update_request = BeneficiaryUpdateRequest(status="active")
|
156
|
+
return await self.update_async(beneficiary_id, update_request)
|