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/client.py ADDED
@@ -0,0 +1,396 @@
1
+ """
2
+ Client for interacting with the Airwallex API.
3
+ """
4
+ import asyncio
5
+ import logging
6
+ import time
7
+ import httpx
8
+ import json
9
+ from datetime import datetime, timedelta, timezone, date
10
+ from typing import Any, Dict, List, Optional, Union, Type, TypeVar, cast
11
+ from importlib import import_module
12
+
13
+ from .utils import snake_to_pascal_case
14
+ from .exceptions import create_exception_from_response, AuthenticationError
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ DEFAULT_BASE_URL = 'https://api.airwallex.com/'
19
+ DEFAULT_AUTH_URL = 'https://api.airwallex.com/api/v1/authentication/login'
20
+
21
+ T = TypeVar("T")
22
+
23
+
24
+ class AirwallexClient:
25
+ """
26
+ Client for interacting with the Airwallex API.
27
+
28
+ This client handles authentication, rate limiting, and provides
29
+ access to all API endpoints through dynamic attribute access.
30
+ """
31
+ def __init__(
32
+ self,
33
+ *,
34
+ client_id: str,
35
+ api_key: str,
36
+ base_url: str = DEFAULT_BASE_URL,
37
+ auth_url: str = DEFAULT_AUTH_URL,
38
+ request_timeout: int = 60,
39
+ on_behalf_of: Optional[str] = None
40
+ ):
41
+ if not client_id or not api_key:
42
+ raise ValueError("Client ID and API key are required")
43
+
44
+ self.client_id = client_id
45
+ self.api_key = api_key
46
+ self.base_url = base_url
47
+ self.auth_url = auth_url
48
+ self.request_timeout = request_timeout
49
+ self.on_behalf_of = on_behalf_of
50
+
51
+ # Authentication state
52
+ self._token: Optional[str] = None
53
+ self._token_expiry: Optional[datetime] = None
54
+
55
+ # Create persistent httpx client
56
+ self._client = httpx.Client(
57
+ base_url=self.base_url,
58
+ timeout=self.request_timeout,
59
+ )
60
+
61
+ # Cache for API instances
62
+ self._api_instances: Dict[str, Any] = {}
63
+
64
+ @property
65
+ def headers(self) -> Dict[str, str]:
66
+ """Default headers to use for all requests."""
67
+ headers = {
68
+ "Content-Type": "application/json",
69
+ "Accept": "application/json",
70
+ }
71
+
72
+ # Add authentication token if available
73
+ if self._token:
74
+ headers["Authorization"] = f"Bearer {self._token}"
75
+
76
+ # Add on-behalf-of header if specified
77
+ if self.on_behalf_of:
78
+ headers["x-on-behalf-of"] = self.on_behalf_of
79
+
80
+ return headers
81
+
82
+ @staticmethod
83
+ def _prepare_params(params: Dict[str, Any]) -> Dict[str, str]:
84
+ """Convert parameters to string format for URL encoding.
85
+
86
+ datetime objects are formatted as ISO8601 strings.
87
+ Lists are joined by commas.
88
+ All other types are converted to strings.
89
+
90
+ Args:
91
+ params (Dict[str, Any]): _description_
92
+
93
+ Returns:
94
+ Dict[str, str]: _description_
95
+ """
96
+ prepared_params = {}
97
+ for key, value in params.items():
98
+ if isinstance(value, (date, datetime)):
99
+ prepared_params[key] = value.isoformat()
100
+ elif isinstance(value, list):
101
+ prepared_params[key] = ",".join(map(str, value))
102
+ else:
103
+ prepared_params[key] = str(value)
104
+ return prepared_params
105
+
106
+ def _prepare_request(self, **kwargs) -> Dict[str, Any]:
107
+ """Merge default headers and allow caller overrides."""
108
+ headers = kwargs.pop('headers', {})
109
+ params = kwargs.pop('params', {})
110
+ kwargs['headers'] = {**self.headers, **headers}
111
+ kwargs['params'] = self._prepare_params(params)
112
+ return kwargs
113
+
114
+ def authenticate(self) -> None:
115
+ """
116
+ Authenticate with the Airwallex API and get an access token.
117
+
118
+ Airwallex auth requires sending the API key and client ID in headers
119
+ and returns a token valid for 30 minutes.
120
+ """
121
+ # Return early if we already have a valid token
122
+ if self._token and self._token_expiry and datetime.now(timezone.utc) < self._token_expiry:
123
+ return
124
+
125
+ # Use a separate client for authentication to avoid base_url issues
126
+ auth_client = httpx.Client(timeout=self.request_timeout)
127
+ try:
128
+ # Airwallex requires x-client-id and x-api-key in the headers, not in the body
129
+ response = auth_client.post(
130
+ self.auth_url,
131
+ headers={
132
+ "Content-Type": "application/json",
133
+ "x-client-id": self.client_id,
134
+ "x-api-key": self.api_key
135
+ }
136
+ )
137
+
138
+ if response.status_code != 201: # Airwallex returns 201 for successful auth
139
+ raise AuthenticationError(
140
+ status_code=response.status_code,
141
+ response=response,
142
+ method="POST",
143
+ url=self.auth_url,
144
+ kwargs={"headers": {"x-client-id": self.client_id, "x-api-key": "**redacted**"}},
145
+ message="Authentication failed"
146
+ )
147
+
148
+ auth_data = response.json()
149
+ self._token = auth_data.get("token")
150
+
151
+ # Set token expiry based on expires_at if provided, or default to 30 minutes
152
+ if "expires_at" in auth_data:
153
+ # Parse ISO8601 format date
154
+ self._token_expiry = datetime.fromisoformat(auth_data["expires_at"].replace("Z", "+00:00"))
155
+ else:
156
+ # Default to 30 minutes if no expires_at provided
157
+ self._token_expiry = datetime.now(timezone.utc) + timedelta(minutes=30)
158
+
159
+ logger.debug("Successfully authenticated with Airwallex API")
160
+
161
+ finally:
162
+ auth_client.close()
163
+
164
+ def _request(self, method: str, url: str, **kwargs) -> Optional[httpx.Response]:
165
+ """
166
+ Make a synchronous HTTP request with automatic authentication.
167
+
168
+ Args:
169
+ method: HTTP method (GET, POST, PUT, DELETE, etc.)
170
+ url: API endpoint URL (relative to base_url)
171
+ **kwargs: Additional arguments to pass to httpx.request()
172
+
173
+ Returns:
174
+ httpx.Response: The HTTP response
175
+
176
+ Raises:
177
+ AirwallexAPIError: For API errors
178
+ """
179
+ # Ensure we're authenticated before making a request
180
+ self.authenticate()
181
+
182
+ retries = 5
183
+ kwargs = self._prepare_request(**kwargs)
184
+
185
+ while retries > 0:
186
+ response = self._client.request(method, url, **kwargs)
187
+
188
+ # Handle successful responses
189
+ if 200 <= response.status_code < 300:
190
+ return response
191
+
192
+ # Handle authentication errors
193
+ if response.status_code == 401:
194
+ # Token might be expired, force refresh and retry
195
+ self._token = None
196
+ self._token_expiry = None
197
+ self.authenticate()
198
+ kwargs['headers'].update({"Authorization": f"Bearer {self._token}"})
199
+ retries -= 1
200
+ continue
201
+
202
+ # Handle rate limiting
203
+ if response.status_code == 429:
204
+ retry_after = response.headers.get('Retry-After')
205
+ if retry_after and retry_after.isdigit():
206
+ wait_time = int(retry_after)
207
+ logger.info(f"Rate limited, sleeping for {wait_time} seconds")
208
+ time.sleep(wait_time)
209
+ continue
210
+ else:
211
+ # Default backoff: 1 second
212
+ time.sleep(1)
213
+ continue
214
+
215
+ # Retry on server errors (HTTP 5xx)
216
+ if response.status_code >= 500 and retries > 0:
217
+ retries -= 1
218
+ logger.warning(f"Server error ({response.status_code}), retrying {retries} more time(s)...")
219
+ time.sleep(1)
220
+ continue
221
+
222
+ # Create and raise the appropriate exception based on the response
223
+ raise create_exception_from_response(
224
+ response=response,
225
+ method=method,
226
+ url=url,
227
+ kwargs=kwargs
228
+ )
229
+
230
+ def __getattr__(self, item: str) -> Any:
231
+ """
232
+ Dynamically load an API wrapper from the `api` subpackage.
233
+ For example, accessing `client.account` will load the Account API wrapper.
234
+ """
235
+ # Check cache first
236
+ if item in self._api_instances:
237
+ return self._api_instances[item]
238
+
239
+ try:
240
+ base_package = self.__class__.__module__.split(".")[0]
241
+ module = import_module(f"{base_package}.api.{item.lower()}")
242
+ # Expect the API class to have the same name but capitalized.
243
+ api_class = getattr(module, snake_to_pascal_case(item))
244
+ api_instance = api_class(client=self)
245
+
246
+ # Cache the instance
247
+ self._api_instances[item] = api_instance
248
+ return api_instance
249
+ except (ModuleNotFoundError, AttributeError) as e:
250
+ raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{item}'") from e
251
+
252
+ def close(self) -> None:
253
+ """Close the HTTP client."""
254
+ self._client.close()
255
+
256
+ def __enter__(self) -> "AirwallexClient":
257
+ return self
258
+
259
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
260
+ self.close()
261
+
262
+
263
+ class AirwallexAsyncClient(AirwallexClient):
264
+ """
265
+ Asynchronous client for interacting with the Airwallex API.
266
+ """
267
+ def __init__(self, **kwargs):
268
+ super().__init__(**kwargs)
269
+
270
+ # Replace the HTTP client with an async one
271
+ self._client = httpx.AsyncClient(
272
+ base_url=self.base_url,
273
+ timeout=self.request_timeout,
274
+ )
275
+
276
+ async def authenticate(self) -> None:
277
+ """
278
+ Authenticate with the Airwallex API and get an access token.
279
+
280
+ Airwallex auth requires sending the API key and client ID in headers
281
+ and returns a token valid for 30 minutes.
282
+ """
283
+ # Return early if we already have a valid token
284
+ if self._token and self._token_expiry and datetime.now() < self._token_expiry:
285
+ return
286
+
287
+ # Use a separate client for authentication to avoid base_url issues
288
+ async with httpx.AsyncClient(timeout=self.request_timeout) as auth_client:
289
+ # Airwallex requires x-client-id and x-api-key in the headers, not in the body
290
+ response = await auth_client.post(
291
+ self.auth_url,
292
+ headers={
293
+ "Content-Type": "application/json",
294
+ "x-client-id": self.client_id,
295
+ "x-api-key": self.api_key
296
+ }
297
+ )
298
+
299
+ if response.status_code != 201: # Airwallex returns 201 for successful auth
300
+ raise AuthenticationError(
301
+ status_code=response.status_code,
302
+ response=response,
303
+ method="POST",
304
+ url=self.auth_url,
305
+ kwargs={"headers": {"x-client-id": self.client_id, "x-api-key": "**redacted**"}},
306
+ message="Authentication failed"
307
+ )
308
+
309
+ auth_data = response.json()
310
+ self._token = auth_data.get("token")
311
+
312
+ # Set token expiry based on expires_at if provided, or default to 30 minutes
313
+ if "expires_at" in auth_data:
314
+ # Parse ISO8601 format date
315
+ self._token_expiry = datetime.fromisoformat(auth_data["expires_at"].replace("Z", "+00:00"))
316
+ else:
317
+ # Default to 30 minutes if no expires_at provided
318
+ self._token_expiry = datetime.now() + timedelta(minutes=30)
319
+
320
+ logger.debug("Successfully authenticated with Airwallex API")
321
+
322
+ async def _request(self, method: str, url: str, **kwargs) -> httpx.Response:
323
+ """
324
+ Make an asynchronous HTTP request with automatic authentication.
325
+
326
+ Args:
327
+ method: HTTP method (GET, POST, PUT, DELETE, etc.)
328
+ url: API endpoint URL (relative to base_url)
329
+ **kwargs: Additional arguments to pass to httpx.request()
330
+
331
+ Returns:
332
+ httpx.Response: The HTTP response
333
+
334
+ Raises:
335
+ AirwallexAPIError: For API errors
336
+ """
337
+ # Ensure we're authenticated before making a request
338
+ await self.authenticate()
339
+
340
+ retries = 5
341
+ kwargs = self._prepare_request(**kwargs)
342
+
343
+ while retries > 0:
344
+ response = await self._client.request(method, url, **kwargs)
345
+
346
+ # Handle successful responses
347
+ if 200 <= response.status_code < 300:
348
+ return response
349
+
350
+ # Handle authentication errors
351
+ if response.status_code == 401:
352
+ # Token might be expired, force refresh and retry
353
+ self._token = None
354
+ self._token_expiry = None
355
+ await self.authenticate()
356
+ kwargs['headers'].update({"Authorization": f"Bearer {self._token}"})
357
+ retries -= 1
358
+ continue
359
+
360
+ # Handle rate limiting
361
+ if response.status_code == 429:
362
+ retry_after = response.headers.get('Retry-After')
363
+ if retry_after and retry_after.isdigit():
364
+ wait_time = int(retry_after)
365
+ logger.info(f"Rate limited, sleeping for {wait_time} seconds")
366
+ await asyncio.sleep(wait_time)
367
+ continue
368
+ else:
369
+ # Default backoff: 1 second
370
+ await asyncio.sleep(1)
371
+ continue
372
+
373
+ # Retry on server errors (HTTP 5xx)
374
+ if response.status_code >= 500 and retries > 0:
375
+ retries -= 1
376
+ logger.warning(f"Server error ({response.status_code}), retrying {retries} more time(s)...")
377
+ await asyncio.sleep(1)
378
+ continue
379
+
380
+ # Create and raise the appropriate exception based on the response
381
+ raise create_exception_from_response(
382
+ response=response,
383
+ method=method,
384
+ url=url,
385
+ kwargs=kwargs
386
+ )
387
+
388
+ async def close(self) -> None:
389
+ """Close the async HTTP client."""
390
+ await self._client.aclose()
391
+
392
+ async def __aenter__(self) -> "AirwallexAsyncClient":
393
+ return self
394
+
395
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
396
+ await self.close()
@@ -0,0 +1,222 @@
1
+ """
2
+ Exceptions for the Airwallex API client.
3
+ """
4
+ from typing import Any, Dict, Optional, Type, ClassVar, Mapping
5
+ import httpx
6
+
7
+
8
+ class AirwallexAPIError(Exception):
9
+ """Base exception for Airwallex API errors."""
10
+
11
+ def __init__(
12
+ self,
13
+ *,
14
+ status_code: int,
15
+ response: httpx.Response,
16
+ method: str,
17
+ url: str,
18
+ kwargs: Dict[str, Any],
19
+ message: Optional[str] = None
20
+ ):
21
+ self.status_code = status_code
22
+ self.response = response
23
+ self.method = method
24
+ self.url = url
25
+ self.kwargs = kwargs
26
+
27
+ # Try to parse error details from the response
28
+ try:
29
+ error_data = response.json()
30
+ self.error_code = error_data.get("code", "unknown")
31
+ self.error_message = error_data.get("message", "Unknown error")
32
+ self.error_source = error_data.get("source", None)
33
+ self.request_id = error_data.get("request_id", None)
34
+ except Exception:
35
+ self.error_code = "unknown"
36
+ self.error_message = message or f"HTTP {status_code} error"
37
+ self.error_source = None
38
+ self.request_id = None
39
+
40
+ super().__init__(self.__str__())
41
+
42
+ def __str__(self) -> str:
43
+ source_info = f" (source: {self.error_source})" if self.error_source else ""
44
+ return (
45
+ f"Airwallex API Error (HTTP {self.status_code}): [{self.error_code}] {self.error_message}{source_info} "
46
+ f"for {self.method} {self.url}"
47
+ )
48
+
49
+
50
+ class AuthenticationError(AirwallexAPIError):
51
+ """Raised when there's an authentication issue."""
52
+ pass
53
+
54
+
55
+ class RateLimitError(AirwallexAPIError):
56
+ """Raised when the API rate limit has been exceeded."""
57
+ pass
58
+
59
+
60
+ class ResourceNotFoundError(AirwallexAPIError):
61
+ """Raised when a requested resource is not found."""
62
+ pass
63
+
64
+
65
+ class ValidationError(AirwallexAPIError):
66
+ """Raised when the request data is invalid."""
67
+ pass
68
+
69
+
70
+ class ServerError(AirwallexAPIError):
71
+ """Raised when the server returns a 5xx error."""
72
+ pass
73
+
74
+
75
+ class ResourceExistsError(ValidationError):
76
+ """Raised when trying to create a resource that already exists."""
77
+ pass
78
+
79
+
80
+ class AmountLimitError(ValidationError):
81
+ """Raised when a transaction amount exceeds or falls below the allowed limits."""
82
+ pass
83
+
84
+
85
+ class EditForbiddenError(ValidationError):
86
+ """Raised when trying to edit a resource that can't be modified."""
87
+ pass
88
+
89
+
90
+ class CurrencyError(ValidationError):
91
+ """Raised for currency-related errors like invalid pairs or unsupported currencies."""
92
+ pass
93
+
94
+
95
+ class DateError(ValidationError):
96
+ """Raised for date-related validation errors."""
97
+ pass
98
+
99
+
100
+ class TransferMethodError(ValidationError):
101
+ """Raised when the transfer method is not supported."""
102
+ pass
103
+
104
+
105
+ class ConversionError(AirwallexAPIError):
106
+ """Raised for conversion-related errors."""
107
+ pass
108
+
109
+
110
+ class ServiceUnavailableError(ServerError):
111
+ """Raised when a service is temporarily unavailable."""
112
+ pass
113
+
114
+
115
+ # Mapping of error codes to exception classes
116
+ ERROR_CODE_MAP: Dict[str, Type[AirwallexAPIError]] = {
117
+ # Authentication errors
118
+ "credentials_expired": AuthenticationError,
119
+ "credentials_invalid": AuthenticationError,
120
+
121
+ # Rate limiting
122
+ "too_many_requests": RateLimitError,
123
+
124
+ # Resource exists
125
+ "already_exists": ResourceExistsError,
126
+
127
+ # Amount limit errors
128
+ "amount_above_limit": AmountLimitError,
129
+ "amount_below_limit": AmountLimitError,
130
+ "amount_above_transfer_method_limit": AmountLimitError,
131
+
132
+ # Edit forbidden
133
+ "can_not_be_edited": EditForbiddenError,
134
+
135
+ # Conversion errors
136
+ "conversion_create_failed": ConversionError,
137
+
138
+ # Validation errors
139
+ "field_required": ValidationError,
140
+ "invalid_argument": ValidationError,
141
+ "term_agreement_is_required": ValidationError,
142
+
143
+ # Currency errors
144
+ "invalid_currency_pair": CurrencyError,
145
+ "unsupported_currency": CurrencyError,
146
+
147
+ # Date errors
148
+ "invalid_transfer_date": DateError,
149
+ "invalid_conversion_date": DateError,
150
+
151
+ # Transfer method errors
152
+ "unsupported_country_code": TransferMethodError,
153
+ "unsupported_transfer_method": TransferMethodError,
154
+
155
+ # Service unavailable
156
+ "service_unavailable": ServiceUnavailableError,
157
+ }
158
+
159
+
160
+ def create_exception_from_response(
161
+ *,
162
+ response: httpx.Response,
163
+ method: str,
164
+ url: str,
165
+ kwargs: Dict[str, Any],
166
+ message: Optional[str] = None
167
+ ) -> AirwallexAPIError:
168
+ """
169
+ Create the appropriate exception based on the API response.
170
+
171
+ This function first checks for specific error codes in the response body.
172
+ If no specific error code is found or it's not recognized, it falls back
173
+ to using the HTTP status code to determine the exception type.
174
+
175
+ Args:
176
+ response: The HTTP response
177
+ method: HTTP method used for the request
178
+ url: URL of the request
179
+ kwargs: Additional keyword arguments passed to the request
180
+ message: Optional custom error message
181
+
182
+ Returns:
183
+ An instance of the appropriate AirwallexAPIError subclass
184
+ """
185
+ status_code = response.status_code
186
+
187
+ try:
188
+ error_data = response.json()
189
+ error_code = error_data.get("code")
190
+
191
+ if error_code and error_code in ERROR_CODE_MAP:
192
+ exception_class = ERROR_CODE_MAP[error_code]
193
+ else:
194
+ # Fall back to status code-based exception
195
+ exception_class = exception_for_status(status_code)
196
+ except Exception:
197
+ # If we can't parse the response JSON, fall back to status code
198
+ exception_class = exception_for_status(status_code)
199
+
200
+ return exception_class(
201
+ status_code=status_code,
202
+ response=response,
203
+ method=method,
204
+ url=url,
205
+ kwargs=kwargs,
206
+ message=message
207
+ )
208
+
209
+
210
+ def exception_for_status(status_code: int) -> Type[AirwallexAPIError]:
211
+ """Return the appropriate exception class for a given HTTP status code."""
212
+ if status_code == 401:
213
+ return AuthenticationError
214
+ elif status_code == 429:
215
+ return RateLimitError
216
+ elif status_code == 404:
217
+ return ResourceNotFoundError
218
+ elif 400 <= status_code < 500:
219
+ return ValidationError
220
+ elif 500 <= status_code < 600:
221
+ return ServerError
222
+ return AirwallexAPIError # Default to the base exception for other status codes
@@ -0,0 +1,69 @@
1
+ """
2
+ Pydantic models for the Airwallex API.
3
+ """
4
+ from .base import AirwallexModel
5
+ from .account import Account as AccountModel
6
+ from .payment import Payment as PaymentModel
7
+ from .beneficiary import Beneficiary as BeneficiaryModel
8
+ from .invoice import Invoice as InvoiceModel, InvoiceItem
9
+ from .financial_transaction import FinancialTransaction as FinancialTransactionModel
10
+ from .fx import FXConversion, FXQuote
11
+ from .account_detail import (
12
+ AccountDetailModel, AccountCreateRequest, AccountUpdateRequest,
13
+ Amendment, AmendmentCreateRequest, WalletInfo, TermsAndConditionsRequest
14
+ )
15
+
16
+ # Issuing API Models
17
+ from .issuing_common import (
18
+ Address,
19
+ Name,
20
+ Merchant,
21
+ RiskDetails,
22
+ DeviceInformation,
23
+ TransactionUsage,
24
+ DeliveryDetails,
25
+ HasMoreResponse
26
+ )
27
+ from .issuing_authorization import Authorization as IssuingAuthorizationModel
28
+ from .issuing_cardholder import Cardholder as IssuingCardholderModel
29
+ from .issuing_card import Card as IssuingCardModel, CardDetails
30
+ from .issuing_digital_wallet_token import DigitalWalletToken as IssuingDigitalWalletTokenModel
31
+ from .issuing_transaction_dispute import TransactionDispute as IssuingTransactionDisputeModel
32
+ from .issuing_transaction import Transaction as IssuingTransactionModel
33
+ from .issuing_config import IssuingConfig as IssuingConfigModel
34
+
35
+ __all__ = [
36
+ "AirwallexModel",
37
+ "AccountModel",
38
+ "PaymentModel",
39
+ "BeneficiaryModel",
40
+ "InvoiceModel",
41
+ "InvoiceItem",
42
+ "FinancialTransactionModel",
43
+ "FXConversion",
44
+ "FXQuote",
45
+ "AccountDetailModel",
46
+ "AccountCreateRequest",
47
+ "AccountUpdateRequest",
48
+ "Amendment",
49
+ "AmendmentCreateRequest",
50
+ "WalletInfo",
51
+ "TermsAndConditionsRequest",
52
+ # Issuing API
53
+ "Address",
54
+ "Name",
55
+ "Merchant",
56
+ "RiskDetails",
57
+ "DeviceInformation",
58
+ "TransactionUsage",
59
+ "DeliveryDetails",
60
+ "HasMoreResponse",
61
+ "IssuingAuthorizationModel",
62
+ "IssuingCardholderModel",
63
+ "IssuingCardModel",
64
+ "CardDetails",
65
+ "IssuingDigitalWalletTokenModel",
66
+ "IssuingTransactionDisputeModel",
67
+ "IssuingTransactionModel",
68
+ "IssuingConfigModel",
69
+ ]