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