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 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()