swifttrack 0.1.3.dev33__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.
- swifttrack/__init__.py +31 -0
- swifttrack/account.py +186 -0
- swifttrack/address.py +174 -0
- swifttrack/auth.py +78 -0
- swifttrack/client.py +229 -0
- swifttrack/config.py +107 -0
- swifttrack/exceptions.py +128 -0
- swifttrack/models/__init__.py +33 -0
- swifttrack/models/account.py +101 -0
- swifttrack/models/address.py +68 -0
- swifttrack/models/auth.py +63 -0
- swifttrack/models/order.py +281 -0
- swifttrack/order.py +223 -0
- swifttrack/py.typed +0 -0
- swifttrack/utils/__init__.py +6 -0
- swifttrack/utils/http_client.py +223 -0
- swifttrack/utils/retry.py +126 -0
- swifttrack-0.1.3.dev33.dist-info/METADATA +380 -0
- swifttrack-0.1.3.dev33.dist-info/RECORD +22 -0
- swifttrack-0.1.3.dev33.dist-info/WHEEL +5 -0
- swifttrack-0.1.3.dev33.dist-info/licenses/LICENSE +21 -0
- swifttrack-0.1.3.dev33.dist-info/top_level.txt +1 -0
swifttrack/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""SwiftTrack Python SDK.
|
|
2
|
+
|
|
3
|
+
A production-grade Python SDK for the SwiftTrack logistics platform.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from swifttrack.client import SwiftTrackClient
|
|
7
|
+
from swifttrack.config import SwiftTrackConfig
|
|
8
|
+
from swifttrack.exceptions import (
|
|
9
|
+
APIError,
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
NotFoundError,
|
|
12
|
+
PermissionError,
|
|
13
|
+
RateLimitError,
|
|
14
|
+
ServerError,
|
|
15
|
+
SwiftTrackError,
|
|
16
|
+
ValidationError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
20
|
+
__all__ = [
|
|
21
|
+
"SwiftTrackClient",
|
|
22
|
+
"SwiftTrackConfig",
|
|
23
|
+
"SwiftTrackError",
|
|
24
|
+
"AuthenticationError",
|
|
25
|
+
"PermissionError",
|
|
26
|
+
"RateLimitError",
|
|
27
|
+
"APIError",
|
|
28
|
+
"ValidationError",
|
|
29
|
+
"NotFoundError",
|
|
30
|
+
"ServerError",
|
|
31
|
+
]
|
swifttrack/account.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Account service for SwiftTrack SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from swifttrack.models.account import (
|
|
10
|
+
Account,
|
|
11
|
+
AccountType,
|
|
12
|
+
CreateAccountRequest,
|
|
13
|
+
LedgerTransaction,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from swifttrack.utils.http_client import HTTPClient
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AccountService:
|
|
23
|
+
"""Service for account/wallet operations."""
|
|
24
|
+
|
|
25
|
+
BASE_PATH = "/api/accounts/v1"
|
|
26
|
+
|
|
27
|
+
def __init__(self, http_client: HTTPClient) -> None:
|
|
28
|
+
self._client = http_client
|
|
29
|
+
|
|
30
|
+
def get_my_account(self, user_id: UUID | str) -> Account:
|
|
31
|
+
"""Get the authenticated user's account.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
user_id: User ID.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Account object.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
NotFoundError: If account doesn't exist.
|
|
41
|
+
AuthenticationError: If user is not authenticated.
|
|
42
|
+
"""
|
|
43
|
+
user_uuid = UUID(user_id) if isinstance(user_id, str) else user_id
|
|
44
|
+
logger.debug(f"Fetching account for user: {user_uuid}")
|
|
45
|
+
|
|
46
|
+
response = self._client.get(
|
|
47
|
+
f"{self.BASE_PATH}/getMyAccount",
|
|
48
|
+
params={"userId": str(user_uuid)},
|
|
49
|
+
)
|
|
50
|
+
account = Account.model_validate(response)
|
|
51
|
+
|
|
52
|
+
logger.info(f"Retrieved account: {account.id}")
|
|
53
|
+
return account
|
|
54
|
+
|
|
55
|
+
def get_transactions(
|
|
56
|
+
self,
|
|
57
|
+
account_id: UUID | str,
|
|
58
|
+
limit: int = 50,
|
|
59
|
+
offset: int = 0,
|
|
60
|
+
) -> list[LedgerTransaction]:
|
|
61
|
+
"""Get transactions for an account.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
account_id: Account ID.
|
|
65
|
+
limit: Number of transactions to return (1-100).
|
|
66
|
+
offset: Pagination offset.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
List of LedgerTransaction objects.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
NotFoundError: If account doesn't exist.
|
|
73
|
+
AuthenticationError: If user is not authenticated.
|
|
74
|
+
PermissionError: If user doesn't have access to account.
|
|
75
|
+
"""
|
|
76
|
+
account_uuid = UUID(account_id) if isinstance(account_id, str) else account_id
|
|
77
|
+
logger.debug(f"Fetching transactions for account: {account_uuid}")
|
|
78
|
+
|
|
79
|
+
response = self._client.get(
|
|
80
|
+
f"{self.BASE_PATH}/getTransactions",
|
|
81
|
+
params={"accountId": str(account_uuid), "limit": limit, "offset": offset},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
transactions = [LedgerTransaction.model_validate(item) for item in response]
|
|
85
|
+
|
|
86
|
+
logger.info(f"Retrieved {len(transactions)} transactions")
|
|
87
|
+
return transactions
|
|
88
|
+
|
|
89
|
+
def create_account(
|
|
90
|
+
self,
|
|
91
|
+
user_id: UUID | str,
|
|
92
|
+
account_type: AccountType,
|
|
93
|
+
) -> Account:
|
|
94
|
+
"""Create a new account for a user.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
user_id: User ID.
|
|
98
|
+
account_type: Type of account to create.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Created Account object.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ValidationError: If account already exists.
|
|
105
|
+
AuthenticationError: If user is not authenticated.
|
|
106
|
+
PermissionError: If user doesn't have permission.
|
|
107
|
+
"""
|
|
108
|
+
user_uuid = UUID(user_id) if isinstance(user_id, str) else user_id
|
|
109
|
+
request = CreateAccountRequest.model_validate(
|
|
110
|
+
{"userId": user_uuid, "accountType": account_type}
|
|
111
|
+
)
|
|
112
|
+
logger.debug(f"Creating {account_type} account for user: {user_uuid}")
|
|
113
|
+
|
|
114
|
+
response = self._client.post(
|
|
115
|
+
f"{self.BASE_PATH}/createAccount",
|
|
116
|
+
json_data=request.model_dump(by_alias=True, exclude_none=True),
|
|
117
|
+
)
|
|
118
|
+
account = Account.model_validate(response)
|
|
119
|
+
|
|
120
|
+
logger.info(f"Created account: {account.id}")
|
|
121
|
+
return account
|
|
122
|
+
|
|
123
|
+
def reconcile_balance(self, account_id: UUID | str) -> str:
|
|
124
|
+
"""Reconcile account balance from ledger transactions.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
account_id: Account ID to reconcile.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Reconciliation status message.
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
NotFoundError: If account doesn't exist.
|
|
134
|
+
AuthenticationError: If user is not authenticated.
|
|
135
|
+
PermissionError: If user doesn't have access to account.
|
|
136
|
+
"""
|
|
137
|
+
account_uuid = UUID(account_id) if isinstance(account_id, str) else account_id
|
|
138
|
+
logger.debug(f"Reconciling balance for account: {account_uuid}")
|
|
139
|
+
|
|
140
|
+
response: Any = self._client.post(
|
|
141
|
+
f"{self.BASE_PATH}/reconcile",
|
|
142
|
+
params={"accountId": str(account_uuid)},
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if isinstance(response, str):
|
|
146
|
+
message = response
|
|
147
|
+
elif isinstance(response, dict):
|
|
148
|
+
message = str(response.get("message", ""))
|
|
149
|
+
else:
|
|
150
|
+
message = str(response)
|
|
151
|
+
|
|
152
|
+
logger.info(f"Reconciled account: {account_uuid}")
|
|
153
|
+
return message
|
|
154
|
+
|
|
155
|
+
def top_up_wallet(
|
|
156
|
+
self,
|
|
157
|
+
user_id: UUID | str,
|
|
158
|
+
amount: float,
|
|
159
|
+
reference: str | None = None,
|
|
160
|
+
) -> Account:
|
|
161
|
+
"""Top up a user's wallet (admin only).
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
user_id: User ID.
|
|
165
|
+
amount: Amount to add.
|
|
166
|
+
reference: Payment reference ID.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Updated Account object.
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
PermissionError: If user doesn't have admin permissions.
|
|
173
|
+
NotFoundError: If account doesn't exist.
|
|
174
|
+
"""
|
|
175
|
+
user_uuid = UUID(user_id) if isinstance(user_id, str) else user_id
|
|
176
|
+
logger.debug(f"Topping up wallet for user: {user_uuid}, amount: {amount}")
|
|
177
|
+
|
|
178
|
+
params: dict[str, Any] = {"userId": str(user_uuid), "amount": str(amount)}
|
|
179
|
+
if reference:
|
|
180
|
+
params["reference"] = reference
|
|
181
|
+
|
|
182
|
+
response = self._client.post(f"{self.BASE_PATH}/admin/topupWallet", params=params)
|
|
183
|
+
account = Account.model_validate(response)
|
|
184
|
+
|
|
185
|
+
logger.info(f"Topped up wallet for user: {user_uuid}")
|
|
186
|
+
return account
|
swifttrack/address.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Address service for SwiftTrack SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from swifttrack.models.address import Address, AddressRequest
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from swifttrack.utils.http_client import HTTPClient
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AddressService:
|
|
18
|
+
"""Service for address management operations."""
|
|
19
|
+
|
|
20
|
+
BASE_PATH = "/api/order/addresses/v1"
|
|
21
|
+
|
|
22
|
+
def __init__(self, http_client: HTTPClient) -> None:
|
|
23
|
+
self._client = http_client
|
|
24
|
+
|
|
25
|
+
def list_addresses(self) -> list[Address]:
|
|
26
|
+
"""Get all saved addresses for the authenticated user.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
List of Address objects.
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
AuthenticationError: If user is not authenticated.
|
|
33
|
+
"""
|
|
34
|
+
logger.debug("Fetching all addresses")
|
|
35
|
+
|
|
36
|
+
response = self._client.get(self.BASE_PATH)
|
|
37
|
+
addresses = [Address.model_validate(item) for item in response]
|
|
38
|
+
|
|
39
|
+
logger.info(f"Retrieved {len(addresses)} addresses")
|
|
40
|
+
return addresses
|
|
41
|
+
|
|
42
|
+
def get_address(self, address_id: UUID | str) -> Address:
|
|
43
|
+
"""Get a specific address by ID.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
address_id: UUID of the address.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Address object.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
NotFoundError: If address doesn't exist.
|
|
53
|
+
AuthenticationError: If user is not authenticated.
|
|
54
|
+
"""
|
|
55
|
+
address_uuid = UUID(address_id) if isinstance(address_id, str) else address_id
|
|
56
|
+
logger.debug(f"Fetching address: {address_uuid}")
|
|
57
|
+
|
|
58
|
+
response = self._client.get(f"{self.BASE_PATH}/{address_uuid}")
|
|
59
|
+
address = Address.model_validate(response)
|
|
60
|
+
|
|
61
|
+
logger.info(f"Retrieved address: {address_uuid}")
|
|
62
|
+
return address
|
|
63
|
+
|
|
64
|
+
def get_default_address(self) -> Address:
|
|
65
|
+
"""Get the default address for the authenticated user.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Default Address object.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
NotFoundError: If no default address exists.
|
|
72
|
+
AuthenticationError: If user is not authenticated.
|
|
73
|
+
"""
|
|
74
|
+
logger.debug("Fetching default address")
|
|
75
|
+
|
|
76
|
+
response = self._client.get(f"{self.BASE_PATH}/default")
|
|
77
|
+
address = Address.model_validate(response)
|
|
78
|
+
|
|
79
|
+
logger.info(f"Retrieved default address: {address.id}")
|
|
80
|
+
return address
|
|
81
|
+
|
|
82
|
+
def create_address(self, address: AddressRequest) -> Address:
|
|
83
|
+
"""Create a new address.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
address: AddressRequest containing address details.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Created Address object.
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
ValidationError: If address data is invalid.
|
|
93
|
+
AuthenticationError: If user is not authenticated.
|
|
94
|
+
"""
|
|
95
|
+
logger.debug("Creating new address")
|
|
96
|
+
|
|
97
|
+
response = self._client.post(
|
|
98
|
+
self.BASE_PATH,
|
|
99
|
+
json_data=address.model_dump(by_alias=True, exclude_none=True),
|
|
100
|
+
)
|
|
101
|
+
created_address = Address.model_validate(response)
|
|
102
|
+
|
|
103
|
+
logger.info(f"Created address: {created_address.id}")
|
|
104
|
+
return created_address
|
|
105
|
+
|
|
106
|
+
def update_address(self, address_id: UUID | str, address: AddressRequest) -> Address:
|
|
107
|
+
"""Update an existing address.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
address_id: UUID of the address to update.
|
|
111
|
+
address: AddressRequest containing updated details.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Updated Address object.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
NotFoundError: If address doesn't exist.
|
|
118
|
+
ValidationError: If address data is invalid.
|
|
119
|
+
AuthenticationError: If user is not authenticated.
|
|
120
|
+
"""
|
|
121
|
+
address_uuid = UUID(address_id) if isinstance(address_id, str) else address_id
|
|
122
|
+
logger.debug(f"Updating address: {address_uuid}")
|
|
123
|
+
|
|
124
|
+
response = self._client.put(
|
|
125
|
+
f"{self.BASE_PATH}/{address_uuid}",
|
|
126
|
+
json_data=address.model_dump(by_alias=True, exclude_none=True),
|
|
127
|
+
)
|
|
128
|
+
updated_address = Address.model_validate(response)
|
|
129
|
+
|
|
130
|
+
logger.info(f"Updated address: {address_uuid}")
|
|
131
|
+
return updated_address
|
|
132
|
+
|
|
133
|
+
def set_default(self, address_id: UUID | str) -> Address:
|
|
134
|
+
"""Set an address as the default.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
address_id: UUID of the address to set as default.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Updated Address object.
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
NotFoundError: If address doesn't exist.
|
|
144
|
+
AuthenticationError: If user is not authenticated.
|
|
145
|
+
"""
|
|
146
|
+
address_uuid = UUID(address_id) if isinstance(address_id, str) else address_id
|
|
147
|
+
logger.debug(f"Setting default address: {address_uuid}")
|
|
148
|
+
|
|
149
|
+
response = self._client.post(f"{self.BASE_PATH}/{address_uuid}/default")
|
|
150
|
+
updated_address = Address.model_validate(response)
|
|
151
|
+
|
|
152
|
+
logger.info(f"Set default address: {address_uuid}")
|
|
153
|
+
return updated_address
|
|
154
|
+
|
|
155
|
+
def delete_address(self, address_id: UUID | str) -> dict[str, Any]:
|
|
156
|
+
"""Delete an address.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
address_id: UUID of the address to delete.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Response message.
|
|
163
|
+
|
|
164
|
+
Raises:
|
|
165
|
+
NotFoundError: If address doesn't exist.
|
|
166
|
+
AuthenticationError: If user is not authenticated.
|
|
167
|
+
"""
|
|
168
|
+
address_uuid = UUID(address_id) if isinstance(address_id, str) else address_id
|
|
169
|
+
logger.debug(f"Deleting address: {address_uuid}")
|
|
170
|
+
|
|
171
|
+
response = self._client.delete(f"{self.BASE_PATH}/{address_uuid}")
|
|
172
|
+
|
|
173
|
+
logger.info(f"Deleted address: {address_uuid}")
|
|
174
|
+
return response if isinstance(response, dict) else {"message": str(response)}
|
swifttrack/auth.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Authentication service for SwiftTrack SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from swifttrack.models.auth import LoginRequest, LoginResponse, UserDetails
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from swifttrack.utils.http_client import HTTPClient
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthService:
|
|
17
|
+
"""Service for authentication operations."""
|
|
18
|
+
|
|
19
|
+
BASE_PATH = "/api/tenant/api/users/v1"
|
|
20
|
+
|
|
21
|
+
def __init__(self, http_client: HTTPClient) -> None:
|
|
22
|
+
self._client = http_client
|
|
23
|
+
|
|
24
|
+
def login(self, email: str, password: str) -> LoginResponse:
|
|
25
|
+
"""Authenticate with email and password.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
email: User email address.
|
|
29
|
+
password: User password.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
LoginResponse containing the access token.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
AuthenticationError: If credentials are invalid.
|
|
36
|
+
ValidationError: If request validation fails.
|
|
37
|
+
"""
|
|
38
|
+
request = LoginRequest(email=email, password=password)
|
|
39
|
+
logger.debug(f"Authenticating user: {email}")
|
|
40
|
+
|
|
41
|
+
response = self._client.post(
|
|
42
|
+
f"{self.BASE_PATH}/login/emailAndPassword",
|
|
43
|
+
json_data=request.model_dump(by_alias=True),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
login_response = LoginResponse.model_validate(response)
|
|
47
|
+
logger.info(f"Successfully authenticated user: {email}")
|
|
48
|
+
return login_response
|
|
49
|
+
|
|
50
|
+
def get_user_details(self, token: str) -> UserDetails:
|
|
51
|
+
"""Get user details from token.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
token: JWT token to validate.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
UserDetails containing user information.
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
AuthenticationError: If token is invalid.
|
|
61
|
+
"""
|
|
62
|
+
logger.debug("Getting user details from token")
|
|
63
|
+
|
|
64
|
+
response = self._client.post(
|
|
65
|
+
f"{self.BASE_PATH}/getUserDetails",
|
|
66
|
+
params={"token": token},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return UserDetails.model_validate(response)
|
|
70
|
+
|
|
71
|
+
def update_auth_token(self, token: str) -> None:
|
|
72
|
+
"""Update the authentication token for subsequent requests.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
token: New authentication token.
|
|
76
|
+
"""
|
|
77
|
+
self._client.update_token(token)
|
|
78
|
+
logger.debug("Updated authentication token")
|
swifttrack/client.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Main SwiftTrack client for interacting with the API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from types import TracebackType
|
|
9
|
+
|
|
10
|
+
from typing_extensions import Self
|
|
11
|
+
|
|
12
|
+
from swifttrack.account import AccountService
|
|
13
|
+
from swifttrack.address import AddressService
|
|
14
|
+
from swifttrack.auth import AuthService
|
|
15
|
+
from swifttrack.config import SwiftTrackConfig
|
|
16
|
+
from swifttrack.models.auth import LoginResponse
|
|
17
|
+
from swifttrack.order import OrderService
|
|
18
|
+
from swifttrack.utils.http_client import HTTPClient
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SwiftTrackClient:
|
|
24
|
+
"""Main client for interacting with the SwiftTrack API.
|
|
25
|
+
|
|
26
|
+
This client provides a high-level interface to all SwiftTrack API endpoints,
|
|
27
|
+
with automatic authentication, retry logic, and error handling.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
>>> from swifttrack import SwiftTrackClient
|
|
31
|
+
>>> client = SwiftTrackClient(base_url="https://api.example.com")
|
|
32
|
+
>>> client.login("user@email.com", "password")
|
|
33
|
+
>>> addresses = client.addresses.list_addresses()
|
|
34
|
+
>>> order = client.orders.get_order(order_id)
|
|
35
|
+
|
|
36
|
+
The client can be used as a context manager for automatic cleanup:
|
|
37
|
+
>>> with SwiftTrackClient() as client:
|
|
38
|
+
... client.login("user@email.com", "password")
|
|
39
|
+
... # use client...
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
base_url: str | None = None,
|
|
45
|
+
token: str | None = None,
|
|
46
|
+
config: SwiftTrackConfig | None = None,
|
|
47
|
+
timeout: float = 30.0,
|
|
48
|
+
max_retries: int = 3,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Initialize the SwiftTrack client.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
base_url: The base URL for the SwiftTrack API.
|
|
54
|
+
Defaults to https://backend-swifttrack.ajayv.online
|
|
55
|
+
token: Optional authentication token.
|
|
56
|
+
config: Optional SwiftTrackConfig instance. If provided,
|
|
57
|
+
base_url, token, timeout, and max_retries are ignored.
|
|
58
|
+
timeout: Request timeout in seconds (default: 30).
|
|
59
|
+
max_retries: Maximum number of retries for failed requests (default: 3).
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ValueError: If configuration is invalid.
|
|
63
|
+
"""
|
|
64
|
+
if config is not None:
|
|
65
|
+
self._config = config
|
|
66
|
+
else:
|
|
67
|
+
# Use environment or default if base_url not provided
|
|
68
|
+
if base_url is None:
|
|
69
|
+
config = SwiftTrackConfig.from_env()
|
|
70
|
+
base_url = config.base_url
|
|
71
|
+
|
|
72
|
+
self._config = SwiftTrackConfig(
|
|
73
|
+
base_url=base_url or "https://backend-swifttrack.ajayv.online",
|
|
74
|
+
token=token,
|
|
75
|
+
timeout=timeout,
|
|
76
|
+
max_retries=max_retries,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
self._http_client = HTTPClient(self._config)
|
|
80
|
+
|
|
81
|
+
# Initialize services
|
|
82
|
+
self._auth = AuthService(self._http_client)
|
|
83
|
+
self._addresses = AddressService(self._http_client)
|
|
84
|
+
self._orders = OrderService(self._http_client)
|
|
85
|
+
self._accounts = AccountService(self._http_client)
|
|
86
|
+
|
|
87
|
+
self._is_authenticated = self._config.token is not None
|
|
88
|
+
|
|
89
|
+
logger.debug(f"SwiftTrackClient initialized with base_url: {self._config.base_url}")
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def config(self) -> SwiftTrackConfig:
|
|
93
|
+
"""Get the current configuration."""
|
|
94
|
+
return self._config
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def is_authenticated(self) -> bool:
|
|
98
|
+
"""Check if the client has an authentication token."""
|
|
99
|
+
return self._is_authenticated
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def auth(self) -> AuthService:
|
|
103
|
+
"""Access authentication operations."""
|
|
104
|
+
return self._auth
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def addresses(self) -> AddressService:
|
|
108
|
+
"""Access address operations."""
|
|
109
|
+
return self._addresses
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def orders(self) -> OrderService:
|
|
113
|
+
"""Access order operations."""
|
|
114
|
+
return self._orders
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def accounts(self) -> AccountService:
|
|
118
|
+
"""Access account/wallet operations."""
|
|
119
|
+
return self._accounts
|
|
120
|
+
|
|
121
|
+
def login(self, email: str, password: str) -> LoginResponse:
|
|
122
|
+
"""Authenticate with email and password.
|
|
123
|
+
|
|
124
|
+
This method authenticates the user and automatically stores the
|
|
125
|
+
access token for subsequent API calls.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
email: User email address.
|
|
129
|
+
password: User password.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
LoginResponse containing the access token.
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
>>> response = client.login("user@example.com", "password123")
|
|
136
|
+
>>> print(f"Token: {response.access_token}")
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
AuthenticationError: If credentials are invalid.
|
|
140
|
+
ValidationError: If request validation fails.
|
|
141
|
+
"""
|
|
142
|
+
response = self._auth.login(email, password)
|
|
143
|
+
self._set_token(response.access_token)
|
|
144
|
+
return response
|
|
145
|
+
|
|
146
|
+
def set_token(self, token: str) -> Self:
|
|
147
|
+
"""Set or update the authentication token.
|
|
148
|
+
|
|
149
|
+
Use this method to manually set the token if you have it from
|
|
150
|
+
an external source (e.g., stored from a previous session).
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
token: The authentication token.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Self for method chaining.
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
>>> client.set_token("your-jwt-token")
|
|
160
|
+
>>> # or chain it:
|
|
161
|
+
>>> client.set_token("token").addresses.list_addresses()
|
|
162
|
+
"""
|
|
163
|
+
self._set_token(token)
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
def _set_token(self, token: str) -> None:
|
|
167
|
+
"""Internal method to set the token."""
|
|
168
|
+
self._http_client.update_token(token)
|
|
169
|
+
self._config = self._http_client.config
|
|
170
|
+
self._is_authenticated = True
|
|
171
|
+
logger.debug("Authentication token set")
|
|
172
|
+
|
|
173
|
+
def logout(self) -> None:
|
|
174
|
+
"""Clear the authentication token.
|
|
175
|
+
|
|
176
|
+
This method clears the stored token but does not invalidate it
|
|
177
|
+
on the server. Call this when the user logs out of your application.
|
|
178
|
+
"""
|
|
179
|
+
self._http_client.update_token("")
|
|
180
|
+
self._is_authenticated = False
|
|
181
|
+
logger.debug("Logged out, token cleared")
|
|
182
|
+
|
|
183
|
+
def close(self) -> None:
|
|
184
|
+
"""Close the HTTP client and release resources.
|
|
185
|
+
|
|
186
|
+
This method should be called when you're done using the client
|
|
187
|
+
to properly close the underlying HTTP connection pool.
|
|
188
|
+
"""
|
|
189
|
+
self._http_client.close()
|
|
190
|
+
logger.debug("SwiftTrackClient closed")
|
|
191
|
+
|
|
192
|
+
def __enter__(self) -> Self:
|
|
193
|
+
"""Enter context manager."""
|
|
194
|
+
return self
|
|
195
|
+
|
|
196
|
+
def __exit__(
|
|
197
|
+
self,
|
|
198
|
+
exc_type: type[BaseException] | None,
|
|
199
|
+
exc_val: BaseException | None,
|
|
200
|
+
exc_tb: TracebackType | None,
|
|
201
|
+
) -> None:
|
|
202
|
+
"""Exit context manager."""
|
|
203
|
+
self.close()
|
|
204
|
+
|
|
205
|
+
@contextmanager
|
|
206
|
+
def temp_token(self, token: str) -> Iterator[Self]:
|
|
207
|
+
"""Context manager to temporarily use a different token.
|
|
208
|
+
|
|
209
|
+
This is useful for operations that need a different user's token
|
|
210
|
+
without affecting the main client's authentication.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
token: Temporary token to use.
|
|
214
|
+
|
|
215
|
+
Example:
|
|
216
|
+
>>> with client.temp_token("other-user-token"):
|
|
217
|
+
... # operations use the temporary token
|
|
218
|
+
... addresses = client.addresses.list_addresses()
|
|
219
|
+
>>> # original token is restored
|
|
220
|
+
"""
|
|
221
|
+
original_token = self._config.token
|
|
222
|
+
try:
|
|
223
|
+
self._set_token(token)
|
|
224
|
+
yield self
|
|
225
|
+
finally:
|
|
226
|
+
if original_token:
|
|
227
|
+
self._set_token(original_token)
|
|
228
|
+
else:
|
|
229
|
+
self.logout()
|