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.

@@ -0,0 +1,312 @@
1
+ """
2
+ Multi HTTP Resource Module
3
+
4
+ This module provides concurrent HTTP request functionality using ThreadPoolExecutor.
5
+ """
6
+
7
+ import concurrent.futures
8
+ import json
9
+ import ssl
10
+ import urllib.error
11
+ import urllib.parse
12
+ import urllib.request
13
+ from typing import Any, Dict, List, Optional, Tuple, Union
14
+
15
+ from ..config import Config
16
+ from ..constants.sdk_constants import SdkConstants
17
+ from ..exceptions.airalo_exception import NetworkError
18
+
19
+
20
+ class MultiHttpResource:
21
+ """
22
+ Concurrent HTTP client for making multiple API requests in parallel.
23
+
24
+ Uses ThreadPoolExecutor for concurrent request execution.
25
+ """
26
+
27
+ def __init__(self, config: Config):
28
+ """
29
+ Initialize multi HTTP resource.
30
+
31
+ Args:
32
+ config: SDK configuration
33
+ """
34
+ self._config = config
35
+ self._handlers: List[Dict[str, Any]] = []
36
+ self._headers: Dict[str, str] = {}
37
+ self._options: Dict[str, Any] = {}
38
+ self._ignore_ssl = False
39
+ self._timeout = SdkConstants.DEFAULT_TIMEOUT
40
+ self._tag: Optional[str] = None
41
+ self._max_workers = SdkConstants.MAX_CONCURRENT_REQUESTS
42
+
43
+ # Default headers
44
+ self._default_headers: Dict[str, str] = {
45
+ "User-Agent": f"Airalo-Python-SDK/{SdkConstants.VERSION}",
46
+ "airalo-python-sdk": f"{SdkConstants.VERSION}",
47
+ "Accept": "application/json",
48
+ "Content-Type": "application/json",
49
+ }
50
+
51
+ def add(self, method_name: str, args: List[Any]) -> "MultiHttpResource":
52
+ """
53
+ Add a request to the queue.
54
+
55
+ Args:
56
+ method_name: HTTP method name ('get', 'post', 'head')
57
+ args: Method arguments [url, params]
58
+
59
+ Returns:
60
+ Self for method chaining
61
+ """
62
+ from .http_resource import HttpResource
63
+
64
+ # Create HTTP resource for this request
65
+ http_resource = HttpResource(self._config, get_handler=True)
66
+
67
+ if self._ignore_ssl:
68
+ http_resource.ignore_ssl()
69
+
70
+ if self._timeout != SdkConstants.DEFAULT_TIMEOUT:
71
+ http_resource.set_timeout(self._timeout)
72
+
73
+ if self._headers:
74
+ http_resource.set_headers(self._headers)
75
+
76
+ # Get the method and create request
77
+ method = getattr(http_resource, method_name)
78
+ request = method(*args)
79
+
80
+ # Store request with metadata
81
+ handler = {
82
+ "request": request,
83
+ "tag": self._tag if self._tag else len(self._handlers),
84
+ "options": self._options.copy(),
85
+ "ignore_ssl": self._ignore_ssl,
86
+ "timeout": self._timeout,
87
+ "headers": self._merge_headers(),
88
+ }
89
+
90
+ self._handlers.append(handler)
91
+ self._tag = None
92
+
93
+ return self
94
+
95
+ def get(
96
+ self, url: str, params: Optional[Dict[str, Any]] = None
97
+ ) -> "MultiHttpResource":
98
+ """
99
+ Add GET request to queue.
100
+
101
+ Args:
102
+ url: Request URL
103
+ params: Query parameters
104
+
105
+ Returns:
106
+ Self for method chaining
107
+ """
108
+ return self.add("get", [url, params])
109
+
110
+ def post(
111
+ self, url: str, params: Optional[Union[Dict[str, Any], str]] = None
112
+ ) -> "MultiHttpResource":
113
+ """
114
+ Add POST request to queue.
115
+
116
+ Args:
117
+ url: Request URL
118
+ params: Request body
119
+
120
+ Returns:
121
+ Self for method chaining
122
+ """
123
+ return self.add("post", [url, params])
124
+
125
+ def tag(self, name: str = "") -> "MultiHttpResource":
126
+ """
127
+ Set tag for next request.
128
+
129
+ Args:
130
+ name: Tag name
131
+
132
+ Returns:
133
+ Self for method chaining
134
+ """
135
+ if name:
136
+ self._tag = name
137
+ return self
138
+
139
+ def set_headers(
140
+ self, headers: Union[Dict[str, str], List[str]]
141
+ ) -> "MultiHttpResource":
142
+ """
143
+ Set headers for all requests.
144
+
145
+ Args:
146
+ headers: Request headers
147
+
148
+ Returns:
149
+ Self for method chaining
150
+ """
151
+ if isinstance(headers, list):
152
+ # Parse list of header strings
153
+ for header in headers:
154
+ if ":" in header:
155
+ key, value = header.split(":", 1)
156
+ self._headers[key.strip()] = value.strip()
157
+ else:
158
+ self._headers.update(headers)
159
+
160
+ return self
161
+
162
+ def set_timeout(self, timeout: int = 30) -> "MultiHttpResource":
163
+ """
164
+ Set timeout for all requests.
165
+
166
+ Args:
167
+ timeout: Timeout in seconds
168
+
169
+ Returns:
170
+ Self for method chaining
171
+ """
172
+ self._timeout = timeout
173
+ return self
174
+
175
+ def ignore_ssl(self) -> "MultiHttpResource":
176
+ """
177
+ Ignore SSL verification for all requests.
178
+
179
+ Returns:
180
+ Self for method chaining
181
+ """
182
+ self._ignore_ssl = True
183
+ return self
184
+
185
+ def setopt(self, options: Dict[str, Any]) -> "MultiHttpResource":
186
+ """
187
+ Set additional options for requests.
188
+
189
+ Args:
190
+ options: Request options
191
+
192
+ Returns:
193
+ Self for method chaining
194
+ """
195
+ self._options = options
196
+ return self
197
+
198
+ def exec(self) -> Dict[Union[str, int], str]:
199
+ """
200
+ Execute all queued requests concurrently.
201
+
202
+ Returns:
203
+ Dictionary mapping tags to response bodies
204
+ """
205
+ if not self._handlers:
206
+ return {}
207
+
208
+ responses = {}
209
+
210
+ # Use ThreadPoolExecutor for concurrent execution
211
+ with concurrent.futures.ThreadPoolExecutor(
212
+ max_workers=self._max_workers
213
+ ) as executor:
214
+ # Submit all requests
215
+ future_to_tag = {}
216
+ for handler in self._handlers:
217
+ future = executor.submit(self._execute_request, handler)
218
+ future_to_tag[future] = handler["tag"]
219
+
220
+ # Collect results
221
+ for future in concurrent.futures.as_completed(future_to_tag):
222
+ tag = future_to_tag[future]
223
+ try:
224
+ response = future.result()
225
+ responses[tag] = response
226
+ except Exception as e:
227
+ # Store error as response
228
+ responses[tag] = json.dumps(
229
+ {"error": str(e), "type": type(e).__name__}
230
+ )
231
+
232
+ # Clear handlers after execution
233
+ self._handlers = []
234
+ self._headers = {}
235
+ self._options = {}
236
+ self._ignore_ssl = False
237
+ self._timeout = SdkConstants.DEFAULT_TIMEOUT
238
+
239
+ return responses
240
+
241
+ def _execute_request(self, handler: Dict[str, Any]) -> str:
242
+ """
243
+ Execute a single request.
244
+
245
+ Args:
246
+ handler: Request handler dictionary
247
+
248
+ Returns:
249
+ Response body
250
+
251
+ Raises:
252
+ NetworkError: If request fails
253
+ """
254
+ request = handler["request"]
255
+
256
+ # Apply headers
257
+ if isinstance(request, urllib.request.Request):
258
+ for key, value in handler["headers"].items():
259
+ request.add_header(key, value)
260
+
261
+ # Configure SSL context
262
+ context = None
263
+ if handler["ignore_ssl"]:
264
+ context = ssl.create_default_context()
265
+ context.check_hostname = False
266
+ context.verify_mode = ssl.CERT_NONE
267
+
268
+ try:
269
+ # Execute request
270
+ response = urllib.request.urlopen(
271
+ request, timeout=handler["timeout"], context=context
272
+ )
273
+
274
+ # Read and return response
275
+ return response.read().decode("utf-8")
276
+
277
+ except urllib.error.HTTPError as e:
278
+ # Try to read error response
279
+ try:
280
+ return e.read().decode("utf-8")
281
+ except:
282
+ raise NetworkError(f"HTTP {e.code}: {e.reason}", http_status=e.code)
283
+
284
+ except urllib.error.URLError as e:
285
+ raise NetworkError(f"Network error: {e.reason}")
286
+
287
+ except Exception as e:
288
+ raise NetworkError(f"Request failed: {str(e)}")
289
+
290
+ def _merge_headers(self) -> Dict[str, str]:
291
+ """
292
+ Merge default, config, and request headers.
293
+
294
+ Returns:
295
+ Merged headers dictionary
296
+ """
297
+ headers = self._default_headers.copy()
298
+
299
+ # Add config headers
300
+ config_headers = self._config.get_http_headers()
301
+ if isinstance(config_headers, list):
302
+ for header in config_headers:
303
+ if ":" in header:
304
+ key, value = header.split(":", 1)
305
+ headers[key.strip()] = value.strip()
306
+ elif isinstance(config_headers, dict):
307
+ headers.update(config_headers)
308
+
309
+ # Add request-specific headers
310
+ headers.update(self._headers)
311
+
312
+ return headers
@@ -0,0 +1,17 @@
1
+ """
2
+ Service classes for Airalo SDK.
3
+ """
4
+
5
+ from .oauth_service import OAuthService
6
+ from .packages_service import PackagesService
7
+ from .order_service import OrderService
8
+ from .installation_instructions_service import InstallationInstructionsService
9
+ from .topup_service import TopupService
10
+
11
+ __all__ = [
12
+ "OAuthService",
13
+ "PackagesService",
14
+ "OrderService",
15
+ "TopupService",
16
+ "InstallationInstructionsService",
17
+ ]
@@ -0,0 +1,34 @@
1
+ import json
2
+ from typing import Optional, Dict, Any
3
+ from ..constants.api_constants import ApiConstants
4
+ from airalo.exceptions import AiraloException
5
+
6
+
7
+ class CompatibilityDevicesService:
8
+ def __init__(self, config, curl, access_token: str):
9
+ if not access_token:
10
+ raise AiraloException("Invalid access token, please check your credentials")
11
+
12
+ self.config = config
13
+ self.curl = curl
14
+ self.access_token = access_token
15
+ self.base_url = self.config.get_url()
16
+
17
+ def get_compatible_devices(self) -> Optional[Dict[str, Any]]:
18
+ url = self._build_url()
19
+
20
+ headers = {
21
+ "Content-Type": "application/json",
22
+ "Authorization": f"Bearer {self.access_token}",
23
+ }
24
+
25
+ response = self.curl.set_headers(headers).get(url)
26
+ result = json.loads(response)
27
+
28
+ if result.get("data"):
29
+ return result
30
+ else:
31
+ return None
32
+
33
+ def _build_url(self) -> str:
34
+ return f"{self.base_url}{ApiConstants.COMPATIBILITY_SLUG}"
@@ -0,0 +1,69 @@
1
+ import json
2
+ import hashlib
3
+ import re
4
+ from urllib.parse import urlencode
5
+
6
+ from airalo.resources.http_resource import HttpResource
7
+ from airalo.helpers.date_helper import DateHelper
8
+ from airalo.exceptions import AiraloException
9
+ from airalo.helpers.cached import Cached
10
+ from airalo.constants.api_constants import ApiConstants
11
+
12
+
13
+ class ExchangeRatesService:
14
+ def __init__(self, config, curl: HttpResource, access_token: str):
15
+ if not access_token:
16
+ raise AiraloException("Invalid access token, please check your credentials")
17
+
18
+ self.config = config
19
+ self.curl = curl
20
+ self.access_token = access_token
21
+ self.base_url = self.config.get_url()
22
+
23
+ def exchange_rates(self, params: dict = None):
24
+ if params is None:
25
+ params = {}
26
+
27
+ self.validate_exchange_rates_request(params)
28
+ url = self.build_url(params)
29
+
30
+ def fetch_data():
31
+ response = self.curl.set_headers(
32
+ {
33
+ "Accept": "application/json",
34
+ "Authorization": f"Bearer {self.access_token}",
35
+ }
36
+ ).get(url)
37
+ return json.loads(response)
38
+
39
+ result = Cached.get(fetch_data, self.get_key(url, params), 300)
40
+
41
+ return result if result and result.get("data") else None
42
+
43
+ def validate_exchange_rates_request(self, params: dict) -> None:
44
+ if "date" in params and params["date"]:
45
+ if not DateHelper.validate_date(params["date"]):
46
+ raise AiraloException(
47
+ "Please enter a valid date in the format YYYY-MM-DD"
48
+ )
49
+
50
+ if "to" in params and params["to"]:
51
+ if not re.match(r"^([A-Za-z]{3})(?:,([A-Za-z]{3}))*$", params["to"]):
52
+ raise AiraloException(
53
+ "Please enter a comma separated list of currency codes. Each code must have 3 letters"
54
+ )
55
+
56
+ def build_url(self, params: dict) -> str:
57
+ query_params = {}
58
+
59
+ if "date" in params and params["date"]:
60
+ query_params["date"] = params["date"]
61
+ if "to" in params and params["to"]:
62
+ query_params["to"] = params["to"]
63
+
64
+ return f"{self.base_url}{ApiConstants.EXCHANGE_RATES_SLUG}?{urlencode(query_params)}"
65
+
66
+ def get_key(self, url: str, params: dict) -> str:
67
+ headers = self.config.get_http_headers()
68
+ raw_key = f"{url}{json.dumps(params, sort_keys=True)}{json.dumps(headers)}{self.access_token}"
69
+ return hashlib.md5(raw_key.encode()).hexdigest()
@@ -0,0 +1,113 @@
1
+ import json
2
+ from datetime import datetime
3
+
4
+ from ..config import Config
5
+ from ..helpers.signature import Signature
6
+ from ..exceptions.airalo_exception import AiraloException
7
+ from ..resources.http_resource import HttpResource
8
+ from ..constants.api_constants import ApiConstants
9
+ from ..constants.sdk_constants import SdkConstants
10
+ from ..helpers.cloud_sim_share_validator import CloudSimShareValidator
11
+
12
+
13
+ class FutureOrderService:
14
+ def __init__(
15
+ self,
16
+ config: Config,
17
+ http_resource: HttpResource,
18
+ signature: Signature,
19
+ access_token: str,
20
+ ):
21
+ if not access_token:
22
+ raise AiraloException(
23
+ "Invalid access token, please check your credentials."
24
+ )
25
+
26
+ self.config = config
27
+ self.http = http_resource
28
+ self.signature = signature
29
+ self.access_token = access_token
30
+ self.base_url = self.config.get_url()
31
+
32
+ def create_future_order(self, payload: dict) -> dict:
33
+ self._validate_future_order(payload)
34
+ self._validate_cloud_sim_share(payload)
35
+
36
+ payload = {k: v for k, v in payload.items() if v}
37
+
38
+ url = self.base_url + ApiConstants.FUTURE_ORDERS
39
+ headers = self._get_headers(payload)
40
+
41
+ response = self.http.set_headers(headers).post(url, payload)
42
+
43
+ if self.http.code != 200:
44
+ raise AiraloException(
45
+ f"Future order creation failed, status code: {self.http.code}, response: {response}"
46
+ )
47
+
48
+ return json.loads(response)
49
+
50
+ def cancel_future_order(self, payload: dict) -> dict:
51
+ self._validate_cancel_future_order(payload)
52
+
53
+ url = self.base_url + ApiConstants.CANCEL_FUTURE_ORDERS
54
+ headers = self._get_headers(payload)
55
+
56
+ response = self.http.set_headers(headers).post(url, payload)
57
+
58
+ if self.http.code != 200:
59
+ raise AiraloException(
60
+ f"Future order cancellation failed, status code: {self.http.code}, response: {response}"
61
+ )
62
+
63
+ return json.loads(response)
64
+
65
+ def _get_headers(self, payload: dict) -> dict:
66
+ return {
67
+ "Authorization": f"Bearer {self.access_token}",
68
+ "Content-Type": "application/json",
69
+ "airalo-signature": self.signature.get_signature(payload),
70
+ }
71
+
72
+ def _validate_future_order(self, payload: dict) -> None:
73
+ if not payload.get("package_id"):
74
+ raise AiraloException(
75
+ f"The package_id is required, payload: {json.dumps(payload)}"
76
+ )
77
+
78
+ if payload.get("quantity", 0) < 1:
79
+ raise AiraloException(
80
+ f"The quantity is required, payload: {json.dumps(payload)}"
81
+ )
82
+
83
+ if payload["quantity"] > SdkConstants.FUTURE_ORDER_LIMIT:
84
+ raise AiraloException(
85
+ f"The packages count may not be greater than {SdkConstants.BULK_ORDER_LIMIT}"
86
+ )
87
+
88
+ due_date = payload.get("due_date")
89
+ if not due_date:
90
+ raise AiraloException(
91
+ f"The due_date is required (format: Y-m-d H:i), payload: {json.dumps(payload)}"
92
+ )
93
+
94
+ try:
95
+ parsed_date = datetime.strptime(due_date, "%Y-%m-%d %H:%M")
96
+ if parsed_date.strftime("%Y-%m-%d %H:%M") != due_date:
97
+ raise ValueError()
98
+ except ValueError:
99
+ raise AiraloException(
100
+ f"The due_date must be in the format Y-m-d H:i, payload: {json.dumps(payload)}"
101
+ )
102
+
103
+ def _validate_cancel_future_order(self, payload: dict) -> None:
104
+ if (
105
+ not isinstance(payload.get("request_ids"), list)
106
+ or not payload["request_ids"]
107
+ ):
108
+ raise AiraloException(
109
+ f"The request_ids is required, payload: {json.dumps(payload)}"
110
+ )
111
+
112
+ def _validate_cloud_sim_share(self, sim_cloud_share: dict) -> None:
113
+ CloudSimShareValidator.validate(sim_cloud_share)
@@ -0,0 +1,63 @@
1
+ import json
2
+ import hashlib
3
+
4
+ from typing import Optional, Dict, Any
5
+ from ..config import Config
6
+ from ..constants.api_constants import ApiConstants
7
+ from ..constants.sdk_constants import SdkConstants
8
+ from ..helpers.cached import Cached
9
+ from ..resources.http_resource import HttpResource
10
+ from ..exceptions.airalo_exception import AiraloException
11
+
12
+
13
+ class InstallationInstructionsService:
14
+ def __init__(self, config, curl: HttpResource, access_token: str):
15
+ if not access_token:
16
+ raise AiraloException("Invalid access token please check your credentials")
17
+
18
+ self.config = config
19
+ self.curl = curl
20
+ self.access_token = access_token
21
+ self.base_url = self.config.get_url()
22
+
23
+ def get_instructions(self, params=None) -> Optional[Dict[str, Any]]:
24
+ if params is None:
25
+ params = {}
26
+
27
+ url = self._build_url(params)
28
+
29
+ result = Cached.get(
30
+ lambda: self._fetch(url, params),
31
+ self._get_key(url, params),
32
+ SdkConstants.DEFAULT_CACHE_TTL,
33
+ )
34
+
35
+ if result and result["data"]:
36
+ return result
37
+ return None
38
+
39
+ def _fetch(self, url, params):
40
+ headers = {
41
+ "Authorization": f"Bearer {self.access_token}",
42
+ "Accept-Language": params.get("language", ""),
43
+ }
44
+ response = self.curl.set_headers(headers).get(url)
45
+ result = json.loads(response)
46
+ return result
47
+
48
+ def _build_url(self, params):
49
+ if "iccid" not in params:
50
+ raise AiraloException('The parameter "iccid" is required.')
51
+
52
+ iccid = str(params["iccid"])
53
+ url = f"{self.base_url}{ApiConstants.SIMS_SLUG}/{iccid}/{ApiConstants.INSTRUCTIONS_SLUG}"
54
+ return url
55
+
56
+ def _get_key(self, url, params):
57
+ data = (
58
+ url
59
+ + json.dumps(params)
60
+ + json.dumps(self.config.get_http_headers())
61
+ + self.access_token
62
+ )
63
+ return hashlib.md5(data.encode("utf-8")).hexdigest()