airalo-sdk 1.0.1__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.
@@ -0,0 +1,357 @@
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
+ if response_data and "pricing" in response_data:
120
+ result["pricing"] = response_data["pricing"]
121
+
122
+ # Append data
123
+ result["data"].extend(response_data["data"])
124
+
125
+ # Check if we've reached the limit
126
+ if limit and len(result["data"]) >= limit:
127
+ result["data"] = result["data"][:limit]
128
+ break
129
+
130
+ # Check for more pages
131
+ meta = response_data.get("meta", {})
132
+ last_page = meta.get("last_page", current_page)
133
+
134
+ if current_page >= last_page:
135
+ break
136
+
137
+ current_page += 1
138
+
139
+ # Flatten if requested
140
+ if params.get("flat"):
141
+ result = self._flatten(result)
142
+
143
+ return result
144
+
145
+ def _build_url(self, params: Dict[str, Any]) -> str:
146
+ """
147
+ Build API URL with query parameters.
148
+
149
+ Args:
150
+ params: Query parameters
151
+
152
+ Returns:
153
+ Complete URL
154
+ """
155
+ url = self._base_url + ApiConstants.PACKAGES_SLUG + "?"
156
+
157
+ query_params = {}
158
+
159
+ # Include topup packages by default (unless simOnly is True)
160
+ if not params.get("simOnly"):
161
+ query_params["include"] = "topup"
162
+
163
+ # Add filters
164
+ if params.get("type") == "local":
165
+ query_params["filter[type]"] = "local"
166
+ elif params.get("type") == "global":
167
+ query_params["filter[type]"] = "global"
168
+
169
+ if params.get("country"):
170
+ query_params["filter[country]"] = params["country"].upper()
171
+
172
+ if params.get("limit") and params["limit"] > 0:
173
+ query_params["limit"] = params["limit"]
174
+
175
+ # Build query string
176
+ if query_params:
177
+ url += urlencode(query_params)
178
+
179
+ return url
180
+
181
+ def _flatten(self, data: Dict[str, Any]) -> Dict[str, Any]:
182
+ """
183
+ Flatten nested package structure.
184
+
185
+ Args:
186
+ data: Nested package data
187
+
188
+ Returns:
189
+ Flattened package data
190
+ """
191
+ flattened = {"data": [], 'pricing': data.get('pricing', [])}
192
+
193
+ for item in data.get("data", []):
194
+ # Each item represents a country/region
195
+ for operator in item.get("operators", []):
196
+ # Extract country codes
197
+ countries = [
198
+ country.get("country_code")
199
+ for country in operator.get("countries", [])
200
+ ]
201
+
202
+ # Process each package
203
+ for package in operator.get("packages", []):
204
+ image = operator.get("image", {})
205
+
206
+ flattened_package = {
207
+ "package_id": package.get("id"),
208
+ "slug": item.get("slug"),
209
+ "type": package.get("type"),
210
+ "price": package.get("price"),
211
+ "net_price": package.get("net_price"),
212
+ "amount": package.get("amount"),
213
+ "day": package.get("day"),
214
+ "is_unlimited": package.get("is_unlimited"),
215
+ "title": package.get("title"),
216
+ "data": package.get("data"),
217
+ "short_info": package.get("short_info"),
218
+ "voice": package.get("voice"),
219
+ "text": package.get("text"),
220
+ "plan_type": operator.get("plan_type"),
221
+ "activation_policy": operator.get("activation_policy"),
222
+ "operator": {
223
+ "title": operator.get("title"),
224
+ "is_roaming": operator.get("is_roaming"),
225
+ "info": operator.get("info"),
226
+ },
227
+ "countries": countries,
228
+ "image": image.get("url") if image else None,
229
+ "other_info": operator.get("other_info"),
230
+ }
231
+ flattened["data"].append(flattened_package)
232
+
233
+ return flattened
234
+
235
+ def _get_cache_key(self, url: str, params: Dict[str, Any]) -> str:
236
+ """
237
+ Generate cache key for request.
238
+
239
+ Args:
240
+ url: Request URL
241
+ params: Request parameters
242
+
243
+ Returns:
244
+ Cache key
245
+ """
246
+ import hashlib
247
+
248
+ key_data = {
249
+ "url": url,
250
+ "params": params,
251
+ "headers": self._config.get_http_headers(),
252
+ "token": self._access_token[:20], # Use partial token for key
253
+ }
254
+ key_string = json.dumps(key_data, sort_keys=True)
255
+ return f"packages_{hashlib.md5(key_string.encode()).hexdigest()}"
256
+
257
+ # Convenience methods for common queries
258
+
259
+ def get_all_packages(
260
+ self,
261
+ flat: bool = False,
262
+ limit: Optional[int] = None,
263
+ page: Optional[int] = None,
264
+ ) -> Optional[Dict[str, Any]]:
265
+ """
266
+ Get all available packages.
267
+
268
+ Args:
269
+ flat: If True, return flattened response
270
+ limit: Number of results
271
+ page: Page number
272
+
273
+ Returns:
274
+ Packages data or None
275
+ """
276
+ return self.get_packages({"flat": flat, "limit": limit, "page": page})
277
+
278
+ def get_sim_packages(
279
+ self,
280
+ flat: bool = False,
281
+ limit: Optional[int] = None,
282
+ page: Optional[int] = None,
283
+ ) -> Optional[Dict[str, Any]]:
284
+ """
285
+ Get SIM-only packages (excludes topups).
286
+
287
+ Args:
288
+ flat: If True, return flattened response
289
+ limit: Number of results
290
+ page: Page number
291
+
292
+ Returns:
293
+ Packages data or None
294
+ """
295
+ return self.get_packages(
296
+ {"flat": flat, "limit": limit, "page": page, "simOnly": True}
297
+ )
298
+
299
+ def get_local_packages(
300
+ self,
301
+ flat: bool = False,
302
+ limit: Optional[int] = None,
303
+ page: Optional[int] = None,
304
+ ) -> Optional[Dict[str, Any]]:
305
+ """
306
+ Get local packages only.
307
+
308
+ Args:
309
+ flat: If True, return flattened response
310
+ limit: Number of results
311
+ page: Page number
312
+
313
+ Returns:
314
+ Packages data or None
315
+ """
316
+ return self.get_packages(
317
+ {"flat": flat, "limit": limit, "page": page, "type": "local"}
318
+ )
319
+
320
+ def get_global_packages(
321
+ self,
322
+ flat: bool = False,
323
+ limit: Optional[int] = None,
324
+ page: Optional[int] = None,
325
+ ) -> Optional[Dict[str, Any]]:
326
+ """
327
+ Get global packages only.
328
+
329
+ Args:
330
+ flat: If True, return flattened response
331
+ limit: Number of results
332
+ page: Page number
333
+
334
+ Returns:
335
+ Packages data or None
336
+ """
337
+ return self.get_packages(
338
+ {"flat": flat, "limit": limit, "page": page, "type": "global"}
339
+ )
340
+
341
+ def get_country_packages(
342
+ self, country_code: str, flat: bool = False, limit: Optional[int] = None
343
+ ) -> Optional[Dict[str, Any]]:
344
+ """
345
+ Get packages for a specific country.
346
+
347
+ Args:
348
+ country_code: ISO country code (e.g., 'US', 'GB')
349
+ flat: If True, return flattened response
350
+ limit: Number of results
351
+
352
+ Returns:
353
+ Packages data or None
354
+ """
355
+ return self.get_packages(
356
+ {"flat": flat, "limit": limit, "country": country_code.upper()}
357
+ )
@@ -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})