airalo-sdk 1.0.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.
Potentially problematic release.
This version of airalo-sdk might be problematic. Click here for more details.
- airalo/__init__.py +29 -0
- airalo/airalo.py +620 -0
- airalo/config.py +146 -0
- airalo/constants/__init__.py +8 -0
- airalo/constants/api_constants.py +49 -0
- airalo/constants/sdk_constants.py +32 -0
- airalo/exceptions/__init__.py +21 -0
- airalo/exceptions/airalo_exception.py +64 -0
- airalo/helpers/__init__.py +9 -0
- airalo/helpers/cached.py +177 -0
- airalo/helpers/cloud_sim_share_validator.py +89 -0
- airalo/helpers/crypt.py +154 -0
- airalo/helpers/date_helper.py +12 -0
- airalo/helpers/signature.py +119 -0
- airalo/resources/__init__.py +8 -0
- airalo/resources/http_resource.py +324 -0
- airalo/resources/multi_http_resource.py +312 -0
- airalo/services/__init__.py +17 -0
- airalo/services/compatibility_devices_service.py +34 -0
- airalo/services/exchange_rates_service.py +69 -0
- airalo/services/future_order_service.py +113 -0
- airalo/services/installation_instructions_service.py +63 -0
- airalo/services/oauth_service.py +186 -0
- airalo/services/order_service.py +463 -0
- airalo/services/packages_service.py +354 -0
- airalo/services/sim_service.py +349 -0
- airalo/services/topup_service.py +127 -0
- airalo/services/voucher_service.py +138 -0
- airalo_sdk-1.0.0.dist-info/METADATA +939 -0
- airalo_sdk-1.0.0.dist-info/RECORD +33 -0
- airalo_sdk-1.0.0.dist-info/WHEEL +5 -0
- airalo_sdk-1.0.0.dist-info/licenses/LICENSE +21 -0
- airalo_sdk-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Packages Service Module
|
|
3
|
+
|
|
4
|
+
This module handles all package-related API operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
from urllib.parse import urlencode
|
|
10
|
+
|
|
11
|
+
from ..config import Config
|
|
12
|
+
from ..constants.api_constants import ApiConstants
|
|
13
|
+
from ..exceptions.airalo_exception import AiraloException, APIError
|
|
14
|
+
from ..helpers.cached import Cached
|
|
15
|
+
from ..resources.http_resource import HttpResource
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PackagesService:
|
|
19
|
+
"""
|
|
20
|
+
Service for managing package operations.
|
|
21
|
+
|
|
22
|
+
Handles fetching packages with various filters, pagination, and caching.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, config: Config, http_resource: HttpResource, access_token: str):
|
|
26
|
+
"""
|
|
27
|
+
Initialize packages service.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
config: SDK configuration
|
|
31
|
+
http_resource: HTTP client
|
|
32
|
+
access_token: API access token
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
AiraloException: If access token is invalid
|
|
36
|
+
"""
|
|
37
|
+
if not access_token:
|
|
38
|
+
raise AiraloException("Invalid access token, please check your credentials")
|
|
39
|
+
|
|
40
|
+
self._config = config
|
|
41
|
+
self._http = http_resource
|
|
42
|
+
self._access_token = access_token
|
|
43
|
+
self._base_url = config.get_url()
|
|
44
|
+
|
|
45
|
+
def get_packages(
|
|
46
|
+
self, params: Optional[Dict[str, Any]] = None
|
|
47
|
+
) -> Optional[Dict[str, Any]]:
|
|
48
|
+
"""
|
|
49
|
+
Get packages with optional filters.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
params: Query parameters including:
|
|
53
|
+
- flat: If True, return flattened response
|
|
54
|
+
- limit: Number of results per page
|
|
55
|
+
- page: Page number
|
|
56
|
+
- type: 'local' or 'global'
|
|
57
|
+
- country: Country code filter
|
|
58
|
+
- simOnly: If True, exclude topup packages
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Packages data or None if no results
|
|
62
|
+
"""
|
|
63
|
+
params = params or {}
|
|
64
|
+
url = self._build_url(params)
|
|
65
|
+
|
|
66
|
+
# Generate cache key
|
|
67
|
+
cache_key = self._get_cache_key(url, params)
|
|
68
|
+
|
|
69
|
+
# Try to get from cache
|
|
70
|
+
result = Cached.get(
|
|
71
|
+
lambda: self._fetch_packages(url, params),
|
|
72
|
+
cache_key,
|
|
73
|
+
ttl=3600, # Cache for 1 hour
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Return None if no data
|
|
77
|
+
if not result or not result.get("data"):
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
def _fetch_packages(self, url: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
83
|
+
"""
|
|
84
|
+
Fetch packages from API with pagination support.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
url: API URL
|
|
88
|
+
params: Query parameters
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Combined packages data
|
|
92
|
+
"""
|
|
93
|
+
current_page = params.get("page") or 1
|
|
94
|
+
limit = params.get("limit")
|
|
95
|
+
result = {"data": []}
|
|
96
|
+
|
|
97
|
+
while True:
|
|
98
|
+
# Build page URL
|
|
99
|
+
page_url = f"{url}&page={current_page}" if current_page else url
|
|
100
|
+
|
|
101
|
+
# Make request
|
|
102
|
+
self._http.set_headers({"Authorization": f"Bearer {self._access_token}"})
|
|
103
|
+
|
|
104
|
+
response = self._http.get(page_url)
|
|
105
|
+
|
|
106
|
+
if not response:
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
# Parse response
|
|
110
|
+
try:
|
|
111
|
+
response_data = json.loads(response)
|
|
112
|
+
except json.JSONDecodeError:
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
# Check for data
|
|
116
|
+
if not response_data.get("data"):
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
# Append data
|
|
120
|
+
result["data"].extend(response_data["data"])
|
|
121
|
+
|
|
122
|
+
# Check if we've reached the limit
|
|
123
|
+
if limit and len(result["data"]) >= limit:
|
|
124
|
+
result["data"] = result["data"][:limit]
|
|
125
|
+
break
|
|
126
|
+
|
|
127
|
+
# Check for more pages
|
|
128
|
+
meta = response_data.get("meta", {})
|
|
129
|
+
last_page = meta.get("last_page", current_page)
|
|
130
|
+
|
|
131
|
+
if current_page >= last_page:
|
|
132
|
+
break
|
|
133
|
+
|
|
134
|
+
current_page += 1
|
|
135
|
+
|
|
136
|
+
# Flatten if requested
|
|
137
|
+
if params.get("flat"):
|
|
138
|
+
result = self._flatten(result)
|
|
139
|
+
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
def _build_url(self, params: Dict[str, Any]) -> str:
|
|
143
|
+
"""
|
|
144
|
+
Build API URL with query parameters.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
params: Query parameters
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Complete URL
|
|
151
|
+
"""
|
|
152
|
+
url = self._base_url + ApiConstants.PACKAGES_SLUG + "?"
|
|
153
|
+
|
|
154
|
+
query_params = {}
|
|
155
|
+
|
|
156
|
+
# Include topup packages by default (unless simOnly is True)
|
|
157
|
+
if not params.get("simOnly"):
|
|
158
|
+
query_params["include"] = "topup"
|
|
159
|
+
|
|
160
|
+
# Add filters
|
|
161
|
+
if params.get("type") == "local":
|
|
162
|
+
query_params["filter[type]"] = "local"
|
|
163
|
+
elif params.get("type") == "global":
|
|
164
|
+
query_params["filter[type]"] = "global"
|
|
165
|
+
|
|
166
|
+
if params.get("country"):
|
|
167
|
+
query_params["filter[country]"] = params["country"].upper()
|
|
168
|
+
|
|
169
|
+
if params.get("limit") and params["limit"] > 0:
|
|
170
|
+
query_params["limit"] = params["limit"]
|
|
171
|
+
|
|
172
|
+
# Build query string
|
|
173
|
+
if query_params:
|
|
174
|
+
url += urlencode(query_params)
|
|
175
|
+
|
|
176
|
+
return url
|
|
177
|
+
|
|
178
|
+
def _flatten(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
179
|
+
"""
|
|
180
|
+
Flatten nested package structure.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
data: Nested package data
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Flattened package data
|
|
187
|
+
"""
|
|
188
|
+
flattened = {"data": []}
|
|
189
|
+
|
|
190
|
+
for item in data.get("data", []):
|
|
191
|
+
# Each item represents a country/region
|
|
192
|
+
for operator in item.get("operators", []):
|
|
193
|
+
# Extract country codes
|
|
194
|
+
countries = [
|
|
195
|
+
country.get("country_code")
|
|
196
|
+
for country in operator.get("countries", [])
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
# Process each package
|
|
200
|
+
for package in operator.get("packages", []):
|
|
201
|
+
image = operator.get("image", {})
|
|
202
|
+
|
|
203
|
+
flattened_package = {
|
|
204
|
+
"package_id": package.get("id"),
|
|
205
|
+
"slug": item.get("slug"),
|
|
206
|
+
"type": package.get("type"),
|
|
207
|
+
"price": package.get("price"),
|
|
208
|
+
"net_price": package.get("net_price"),
|
|
209
|
+
"amount": package.get("amount"),
|
|
210
|
+
"day": package.get("day"),
|
|
211
|
+
"is_unlimited": package.get("is_unlimited"),
|
|
212
|
+
"title": package.get("title"),
|
|
213
|
+
"data": package.get("data"),
|
|
214
|
+
"short_info": package.get("short_info"),
|
|
215
|
+
"voice": package.get("voice"),
|
|
216
|
+
"text": package.get("text"),
|
|
217
|
+
"plan_type": operator.get("plan_type"),
|
|
218
|
+
"activation_policy": operator.get("activation_policy"),
|
|
219
|
+
"operator": {
|
|
220
|
+
"title": operator.get("title"),
|
|
221
|
+
"is_roaming": operator.get("is_roaming"),
|
|
222
|
+
"info": operator.get("info"),
|
|
223
|
+
},
|
|
224
|
+
"countries": countries,
|
|
225
|
+
"image": image.get("url") if image else None,
|
|
226
|
+
"other_info": operator.get("other_info"),
|
|
227
|
+
}
|
|
228
|
+
flattened["data"].append(flattened_package)
|
|
229
|
+
|
|
230
|
+
return flattened
|
|
231
|
+
|
|
232
|
+
def _get_cache_key(self, url: str, params: Dict[str, Any]) -> str:
|
|
233
|
+
"""
|
|
234
|
+
Generate cache key for request.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
url: Request URL
|
|
238
|
+
params: Request parameters
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Cache key
|
|
242
|
+
"""
|
|
243
|
+
import hashlib
|
|
244
|
+
|
|
245
|
+
key_data = {
|
|
246
|
+
"url": url,
|
|
247
|
+
"params": params,
|
|
248
|
+
"headers": self._config.get_http_headers(),
|
|
249
|
+
"token": self._access_token[:20], # Use partial token for key
|
|
250
|
+
}
|
|
251
|
+
key_string = json.dumps(key_data, sort_keys=True)
|
|
252
|
+
return f"packages_{hashlib.md5(key_string.encode()).hexdigest()}"
|
|
253
|
+
|
|
254
|
+
# Convenience methods for common queries
|
|
255
|
+
|
|
256
|
+
def get_all_packages(
|
|
257
|
+
self,
|
|
258
|
+
flat: bool = False,
|
|
259
|
+
limit: Optional[int] = None,
|
|
260
|
+
page: Optional[int] = None,
|
|
261
|
+
) -> Optional[Dict[str, Any]]:
|
|
262
|
+
"""
|
|
263
|
+
Get all available packages.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
flat: If True, return flattened response
|
|
267
|
+
limit: Number of results
|
|
268
|
+
page: Page number
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Packages data or None
|
|
272
|
+
"""
|
|
273
|
+
return self.get_packages({"flat": flat, "limit": limit, "page": page})
|
|
274
|
+
|
|
275
|
+
def get_sim_packages(
|
|
276
|
+
self,
|
|
277
|
+
flat: bool = False,
|
|
278
|
+
limit: Optional[int] = None,
|
|
279
|
+
page: Optional[int] = None,
|
|
280
|
+
) -> Optional[Dict[str, Any]]:
|
|
281
|
+
"""
|
|
282
|
+
Get SIM-only packages (excludes topups).
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
flat: If True, return flattened response
|
|
286
|
+
limit: Number of results
|
|
287
|
+
page: Page number
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Packages data or None
|
|
291
|
+
"""
|
|
292
|
+
return self.get_packages(
|
|
293
|
+
{"flat": flat, "limit": limit, "page": page, "simOnly": True}
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def get_local_packages(
|
|
297
|
+
self,
|
|
298
|
+
flat: bool = False,
|
|
299
|
+
limit: Optional[int] = None,
|
|
300
|
+
page: Optional[int] = None,
|
|
301
|
+
) -> Optional[Dict[str, Any]]:
|
|
302
|
+
"""
|
|
303
|
+
Get local packages only.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
flat: If True, return flattened response
|
|
307
|
+
limit: Number of results
|
|
308
|
+
page: Page number
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Packages data or None
|
|
312
|
+
"""
|
|
313
|
+
return self.get_packages(
|
|
314
|
+
{"flat": flat, "limit": limit, "page": page, "type": "local"}
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def get_global_packages(
|
|
318
|
+
self,
|
|
319
|
+
flat: bool = False,
|
|
320
|
+
limit: Optional[int] = None,
|
|
321
|
+
page: Optional[int] = None,
|
|
322
|
+
) -> Optional[Dict[str, Any]]:
|
|
323
|
+
"""
|
|
324
|
+
Get global packages only.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
flat: If True, return flattened response
|
|
328
|
+
limit: Number of results
|
|
329
|
+
page: Page number
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Packages data or None
|
|
333
|
+
"""
|
|
334
|
+
return self.get_packages(
|
|
335
|
+
{"flat": flat, "limit": limit, "page": page, "type": "global"}
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def get_country_packages(
|
|
339
|
+
self, country_code: str, flat: bool = False, limit: Optional[int] = None
|
|
340
|
+
) -> Optional[Dict[str, Any]]:
|
|
341
|
+
"""
|
|
342
|
+
Get packages for a specific country.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
country_code: ISO country code (e.g., 'US', 'GB')
|
|
346
|
+
flat: If True, return flattened response
|
|
347
|
+
limit: Number of results
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Packages data or None
|
|
351
|
+
"""
|
|
352
|
+
return self.get_packages(
|
|
353
|
+
{"flat": flat, "limit": limit, "country": country_code.upper()}
|
|
354
|
+
)
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SIM Service Module
|
|
3
|
+
|
|
4
|
+
This module handles all SIM-related API operations including usage tracking,
|
|
5
|
+
topup history, and package history.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from ..config import Config
|
|
12
|
+
from ..helpers.cached import Cached
|
|
13
|
+
from ..resources.http_resource import HttpResource
|
|
14
|
+
from ..constants.api_constants import ApiConstants
|
|
15
|
+
from ..resources.multi_http_resource import MultiHttpResource
|
|
16
|
+
from ..exceptions.airalo_exception import AiraloException, APIError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SimService:
|
|
20
|
+
"""
|
|
21
|
+
Service for managing SIM operations.
|
|
22
|
+
|
|
23
|
+
Handles SIM usage tracking, bulk usage queries, topup history, and package history.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
config: Config,
|
|
29
|
+
http_resource: HttpResource,
|
|
30
|
+
multi_http_resource: MultiHttpResource,
|
|
31
|
+
access_token: str,
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Initialize SIM service.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
config: SDK configuration
|
|
38
|
+
http_resource: HTTP client for single requests
|
|
39
|
+
multi_http_resource: HTTP client for concurrent requests
|
|
40
|
+
access_token: API access token
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
AiraloException: If access token is invalid
|
|
44
|
+
"""
|
|
45
|
+
if not access_token:
|
|
46
|
+
raise AiraloException("Invalid access token, please check your credentials")
|
|
47
|
+
|
|
48
|
+
self._config = config
|
|
49
|
+
self._http = http_resource
|
|
50
|
+
self._multi_http = multi_http_resource
|
|
51
|
+
self._access_token = access_token
|
|
52
|
+
self._base_url = config.get_url()
|
|
53
|
+
|
|
54
|
+
def sim_usage(self, params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
55
|
+
"""
|
|
56
|
+
Get SIM usage information.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
params: Parameters including:
|
|
60
|
+
- iccid: ICCID of the SIM (required)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
SIM usage data or None
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
AiraloException: If ICCID is invalid
|
|
67
|
+
"""
|
|
68
|
+
url = self._build_url(params, ApiConstants.SIMS_USAGE)
|
|
69
|
+
|
|
70
|
+
# Generate cache key
|
|
71
|
+
cache_key = self._get_cache_key(url, params)
|
|
72
|
+
|
|
73
|
+
# Try to get from cache (5 minutes TTL for usage data)
|
|
74
|
+
result = Cached.get(
|
|
75
|
+
lambda: self._fetch_sim_data(url), cache_key, ttl=300 # 5 minutes
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Return None if no data
|
|
79
|
+
if not result or not result.get("data"):
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
def sim_usage_bulk(self, iccids: List[str]) -> Optional[Dict[str, Any]]:
|
|
85
|
+
"""
|
|
86
|
+
Get usage information for multiple SIMs.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
iccids: List of ICCIDs to check
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Dict mapping ICCIDs to usage data or None
|
|
93
|
+
"""
|
|
94
|
+
if not iccids:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
# Generate cache key for bulk request
|
|
98
|
+
cache_key = self._get_cache_key("".join(iccids), {})
|
|
99
|
+
|
|
100
|
+
# Try to get from cache
|
|
101
|
+
result = Cached.get(
|
|
102
|
+
lambda: self._fetch_bulk_sim_usage(iccids), cache_key, ttl=300 # 5 minutes
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
def _fetch_bulk_sim_usage(self, iccids: List[str]) -> Optional[Dict[str, Any]]:
|
|
108
|
+
"""
|
|
109
|
+
Fetch usage data for multiple SIMs concurrently.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
iccids: List of ICCIDs
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Dict mapping ICCIDs to usage data
|
|
116
|
+
"""
|
|
117
|
+
# Queue requests for each ICCID
|
|
118
|
+
for iccid in iccids:
|
|
119
|
+
url = self._build_url({"iccid": iccid}, ApiConstants.SIMS_USAGE)
|
|
120
|
+
|
|
121
|
+
self._multi_http.tag(iccid).set_headers(
|
|
122
|
+
{
|
|
123
|
+
"Content-Type": "application/json",
|
|
124
|
+
"Authorization": f"Bearer {self._access_token}",
|
|
125
|
+
}
|
|
126
|
+
).get(url)
|
|
127
|
+
|
|
128
|
+
# Execute all requests
|
|
129
|
+
responses = self._multi_http.exec()
|
|
130
|
+
|
|
131
|
+
if not responses:
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
# Parse responses
|
|
135
|
+
result = {}
|
|
136
|
+
for iccid, response in responses.items():
|
|
137
|
+
try:
|
|
138
|
+
result[iccid] = json.loads(response)
|
|
139
|
+
except json.JSONDecodeError:
|
|
140
|
+
result[iccid] = {"error": "Failed to parse response", "raw": response}
|
|
141
|
+
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
def sim_topups(self, params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
145
|
+
"""
|
|
146
|
+
Get SIM topup history.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
params: Parameters including:
|
|
150
|
+
- iccid: ICCID of the SIM (required)
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
SIM topup history or None
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
AiraloException: If ICCID is invalid
|
|
157
|
+
"""
|
|
158
|
+
url = self._build_url(params, ApiConstants.SIMS_TOPUPS)
|
|
159
|
+
|
|
160
|
+
# Generate cache key
|
|
161
|
+
cache_key = self._get_cache_key(url, params)
|
|
162
|
+
|
|
163
|
+
# Try to get from cache (5 minutes TTL)
|
|
164
|
+
result = Cached.get(
|
|
165
|
+
lambda: self._fetch_sim_data(url), cache_key, ttl=300 # 5 minutes
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Return None if no data
|
|
169
|
+
if not result or not result.get("data"):
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
def sim_package_history(self, params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
175
|
+
"""
|
|
176
|
+
Get SIM package history.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
params: Parameters including:
|
|
180
|
+
- iccid: ICCID of the SIM (required)
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
SIM package history or None
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
AiraloException: If ICCID is invalid
|
|
187
|
+
"""
|
|
188
|
+
url = self._build_url(params, ApiConstants.SIMS_PACKAGES)
|
|
189
|
+
|
|
190
|
+
# Generate cache key
|
|
191
|
+
cache_key = self._get_cache_key(url, params)
|
|
192
|
+
|
|
193
|
+
# Try to get from cache (15 minutes TTL for package history)
|
|
194
|
+
result = Cached.get(
|
|
195
|
+
lambda: self._fetch_sim_data(url), cache_key, ttl=900 # 15 minutes
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Return None if no data
|
|
199
|
+
if not result or not result.get("data"):
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
def _fetch_sim_data(self, url: str) -> Dict[str, Any]:
|
|
205
|
+
"""
|
|
206
|
+
Fetch SIM data from API.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
url: API URL
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
SIM data
|
|
213
|
+
"""
|
|
214
|
+
# Make request
|
|
215
|
+
self._http.set_headers(
|
|
216
|
+
{
|
|
217
|
+
"Content-Type": "application/json",
|
|
218
|
+
"Authorization": f"Bearer {self._access_token}",
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
response = self._http.get(url)
|
|
223
|
+
|
|
224
|
+
if not response:
|
|
225
|
+
return {"data": None}
|
|
226
|
+
|
|
227
|
+
# Parse response
|
|
228
|
+
try:
|
|
229
|
+
return json.loads(response)
|
|
230
|
+
except json.JSONDecodeError:
|
|
231
|
+
return {"data": None}
|
|
232
|
+
|
|
233
|
+
def _build_url(self, params: Dict[str, Any], endpoint: Optional[str] = None) -> str:
|
|
234
|
+
"""
|
|
235
|
+
Build API URL for SIM operations.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
params: Parameters including ICCID
|
|
239
|
+
endpoint: Specific endpoint (usage, topups, packages)
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Complete URL
|
|
243
|
+
|
|
244
|
+
Raises:
|
|
245
|
+
AiraloException: If ICCID is invalid
|
|
246
|
+
"""
|
|
247
|
+
if "iccid" not in params or not self._is_valid_iccid(params["iccid"]):
|
|
248
|
+
raise AiraloException(f"Invalid or missing ICCID: {params.get('iccid')}")
|
|
249
|
+
|
|
250
|
+
iccid = str(params["iccid"])
|
|
251
|
+
|
|
252
|
+
# Build URL
|
|
253
|
+
url = f"{self._base_url}{ApiConstants.SIMS_SLUG}/{iccid}"
|
|
254
|
+
|
|
255
|
+
if endpoint:
|
|
256
|
+
url = f"{url}/{endpoint}"
|
|
257
|
+
|
|
258
|
+
return url
|
|
259
|
+
|
|
260
|
+
def _is_valid_iccid(self, iccid: Any) -> bool:
|
|
261
|
+
"""
|
|
262
|
+
Validate ICCID format.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
iccid: ICCID to validate
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
True if valid, False otherwise
|
|
269
|
+
"""
|
|
270
|
+
if not iccid:
|
|
271
|
+
return False
|
|
272
|
+
|
|
273
|
+
# Convert to string and check
|
|
274
|
+
iccid_str = str(iccid)
|
|
275
|
+
|
|
276
|
+
# ICCID should be 18-22 digits
|
|
277
|
+
return iccid_str.isdigit() and 18 <= len(iccid_str) <= 22
|
|
278
|
+
|
|
279
|
+
def _get_cache_key(self, url: str, params: Dict[str, Any]) -> str:
|
|
280
|
+
"""
|
|
281
|
+
Generate cache key for request.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
url: Request URL
|
|
285
|
+
params: Request parameters
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Cache key
|
|
289
|
+
"""
|
|
290
|
+
import hashlib
|
|
291
|
+
|
|
292
|
+
key_data = {
|
|
293
|
+
"url": url,
|
|
294
|
+
"params": params,
|
|
295
|
+
"headers": self._config.get_http_headers(),
|
|
296
|
+
"token": self._access_token[:20] if self._access_token else "",
|
|
297
|
+
}
|
|
298
|
+
key_string = json.dumps(key_data, sort_keys=True)
|
|
299
|
+
return f"sim_{hashlib.md5(key_string.encode()).hexdigest()}"
|
|
300
|
+
|
|
301
|
+
# Convenience methods for cleaner API
|
|
302
|
+
|
|
303
|
+
def get_usage(self, iccid: str) -> Optional[Dict[str, Any]]:
|
|
304
|
+
"""
|
|
305
|
+
Get SIM usage (convenience method).
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
iccid: ICCID of the SIM
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
SIM usage data or None
|
|
312
|
+
"""
|
|
313
|
+
return self.sim_usage({"iccid": iccid})
|
|
314
|
+
|
|
315
|
+
def get_usage_bulk(self, iccids: List[str]) -> Optional[Dict[str, Any]]:
|
|
316
|
+
"""
|
|
317
|
+
Get usage for multiple SIMs (convenience method).
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
iccids: List of ICCIDs
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Usage data for all SIMs
|
|
324
|
+
"""
|
|
325
|
+
return self.sim_usage_bulk(iccids)
|
|
326
|
+
|
|
327
|
+
def get_topups(self, iccid: str) -> Optional[Dict[str, Any]]:
|
|
328
|
+
"""
|
|
329
|
+
Get SIM topup history (convenience method).
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
iccid: ICCID of the SIM
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Topup history or None
|
|
336
|
+
"""
|
|
337
|
+
return self.sim_topups({"iccid": iccid})
|
|
338
|
+
|
|
339
|
+
def get_package_history(self, iccid: str) -> Optional[Dict[str, Any]]:
|
|
340
|
+
"""
|
|
341
|
+
Get SIM package history (convenience method).
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
iccid: ICCID of the SIM
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
Package history or None
|
|
348
|
+
"""
|
|
349
|
+
return self.sim_package_history({"iccid": iccid})
|