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.
Files changed (39) hide show
  1. airwallex/__init__.py +74 -0
  2. airwallex/api/__init__.py +37 -0
  3. airwallex/api/account.py +107 -0
  4. airwallex/api/account_detail.py +469 -0
  5. airwallex/api/base.py +488 -0
  6. airwallex/api/beneficiary.py +156 -0
  7. airwallex/api/financial_transaction.py +123 -0
  8. airwallex/api/invoice.py +257 -0
  9. airwallex/api/issuing_authorization.py +313 -0
  10. airwallex/api/issuing_card.py +411 -0
  11. airwallex/api/issuing_cardholder.py +234 -0
  12. airwallex/api/issuing_config.py +80 -0
  13. airwallex/api/issuing_digital_wallet_token.py +249 -0
  14. airwallex/api/issuing_transaction.py +231 -0
  15. airwallex/api/issuing_transaction_dispute.py +339 -0
  16. airwallex/api/payment.py +148 -0
  17. airwallex/client.py +396 -0
  18. airwallex/exceptions.py +222 -0
  19. airwallex/models/__init__.py +69 -0
  20. airwallex/models/account.py +51 -0
  21. airwallex/models/account_detail.py +259 -0
  22. airwallex/models/base.py +121 -0
  23. airwallex/models/beneficiary.py +70 -0
  24. airwallex/models/financial_transaction.py +30 -0
  25. airwallex/models/fx.py +58 -0
  26. airwallex/models/invoice.py +102 -0
  27. airwallex/models/issuing_authorization.py +41 -0
  28. airwallex/models/issuing_card.py +135 -0
  29. airwallex/models/issuing_cardholder.py +52 -0
  30. airwallex/models/issuing_common.py +83 -0
  31. airwallex/models/issuing_config.py +62 -0
  32. airwallex/models/issuing_digital_wallet_token.py +38 -0
  33. airwallex/models/issuing_transaction.py +42 -0
  34. airwallex/models/issuing_transaction_dispute.py +59 -0
  35. airwallex/models/payment.py +81 -0
  36. airwallex/utils.py +107 -0
  37. airwallex_sdk-0.1.0.dist-info/METADATA +202 -0
  38. airwallex_sdk-0.1.0.dist-info/RECORD +39 -0
  39. 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)