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
airalo/config.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration Module
|
|
3
|
+
|
|
4
|
+
This module handles SDK configuration including credentials, environment settings,
|
|
5
|
+
and HTTP headers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any, Dict, List, Optional, Union
|
|
10
|
+
from urllib.parse import urlencode
|
|
11
|
+
|
|
12
|
+
from .constants.api_constants import ApiConstants
|
|
13
|
+
from .exceptions.airalo_exception import ConfigurationError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Config:
|
|
17
|
+
"""
|
|
18
|
+
Configuration class for Airalo SDK.
|
|
19
|
+
|
|
20
|
+
Handles SDK configuration including API credentials, environment selection,
|
|
21
|
+
and custom HTTP headers.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
MANDATORY_CONFIG_KEYS = [
|
|
25
|
+
"client_id",
|
|
26
|
+
"client_secret",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
def __init__(self, data: Union[Dict[str, Any], str, object]):
|
|
30
|
+
"""
|
|
31
|
+
Initialize configuration.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
data: Configuration data as dict, JSON string, or object with attributes
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ConfigurationError: If configuration is invalid or missing required fields
|
|
38
|
+
"""
|
|
39
|
+
self._data: Dict[str, Any] = {}
|
|
40
|
+
|
|
41
|
+
if not data:
|
|
42
|
+
raise ConfigurationError("Config data is not provided")
|
|
43
|
+
|
|
44
|
+
# Convert different input types to dictionary
|
|
45
|
+
if isinstance(data, str):
|
|
46
|
+
try:
|
|
47
|
+
self._data = json.loads(data)
|
|
48
|
+
except json.JSONDecodeError as e:
|
|
49
|
+
raise ConfigurationError(f"Invalid JSON config data: {e}")
|
|
50
|
+
elif isinstance(data, dict):
|
|
51
|
+
self._data = data.copy()
|
|
52
|
+
elif hasattr(data, "__dict__"):
|
|
53
|
+
# Convert object to dictionary
|
|
54
|
+
self._data = vars(data).copy()
|
|
55
|
+
else:
|
|
56
|
+
try:
|
|
57
|
+
# Try to serialize and deserialize to get a dict
|
|
58
|
+
self._data = json.loads(json.dumps(data, default=lambda o: o.__dict__))
|
|
59
|
+
except (TypeError, json.JSONDecodeError) as e:
|
|
60
|
+
raise ConfigurationError(f"Invalid config data provided: {e}")
|
|
61
|
+
|
|
62
|
+
self._validate()
|
|
63
|
+
|
|
64
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
65
|
+
"""
|
|
66
|
+
Get configuration value by key.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
key: Configuration key
|
|
70
|
+
default: Default value if key not found
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Configuration value or default
|
|
74
|
+
"""
|
|
75
|
+
return self._data.get(key, default)
|
|
76
|
+
|
|
77
|
+
def get_config(self) -> Dict[str, Any]:
|
|
78
|
+
"""
|
|
79
|
+
Get complete configuration dictionary.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Configuration dictionary
|
|
83
|
+
"""
|
|
84
|
+
return self._data.copy()
|
|
85
|
+
|
|
86
|
+
def get_credentials(self, as_string: bool = False) -> Union[Dict[str, str], str]:
|
|
87
|
+
"""
|
|
88
|
+
Get API credentials.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
as_string: If True, return as URL-encoded string
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Credentials as dictionary or URL-encoded string
|
|
95
|
+
"""
|
|
96
|
+
credentials = {
|
|
97
|
+
"client_id": self._data["client_id"],
|
|
98
|
+
"client_secret": self._data["client_secret"],
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if as_string:
|
|
102
|
+
return urlencode(credentials)
|
|
103
|
+
|
|
104
|
+
return credentials
|
|
105
|
+
|
|
106
|
+
def get_environment(self) -> str:
|
|
107
|
+
"""
|
|
108
|
+
Get current environment.
|
|
109
|
+
"""
|
|
110
|
+
return self._data.get("env", "production")
|
|
111
|
+
|
|
112
|
+
def get_url(self) -> str:
|
|
113
|
+
"""
|
|
114
|
+
Get base API URL for current environment.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Base API URL
|
|
118
|
+
"""
|
|
119
|
+
return ApiConstants.PRODUCTION_URL
|
|
120
|
+
|
|
121
|
+
def get_http_headers(self) -> List[str]:
|
|
122
|
+
"""
|
|
123
|
+
Get custom HTTP headers.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
List of HTTP header strings
|
|
127
|
+
"""
|
|
128
|
+
return self._data.get("http_headers", [])
|
|
129
|
+
|
|
130
|
+
def _validate(self) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Validate configuration.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ConfigurationError: If configuration is invalid
|
|
136
|
+
"""
|
|
137
|
+
# Check mandatory fields
|
|
138
|
+
for key in self.MANDATORY_CONFIG_KEYS:
|
|
139
|
+
if key not in self._data or not self._data[key]:
|
|
140
|
+
raise ConfigurationError(
|
|
141
|
+
f"Mandatory field `{key}` is missing in the provided config data"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Set default environment if not provided
|
|
145
|
+
if "env" not in self._data:
|
|
146
|
+
self._data["env"] = "production"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API Constants Module
|
|
3
|
+
|
|
4
|
+
This module contains all API endpoints and URLs used by the Airalo SDK.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ApiConstants:
|
|
9
|
+
"""API endpoints and URLs for Airalo SDK."""
|
|
10
|
+
|
|
11
|
+
PRODUCTION_URL = "https://partners-api.airalo.com/v2/"
|
|
12
|
+
|
|
13
|
+
# Authentication
|
|
14
|
+
TOKEN_SLUG = "token"
|
|
15
|
+
|
|
16
|
+
# Package endpoints
|
|
17
|
+
PACKAGES_SLUG = "packages"
|
|
18
|
+
|
|
19
|
+
# Order endpoints
|
|
20
|
+
ORDERS_SLUG = "orders"
|
|
21
|
+
ASYNC_ORDERS_SLUG = "orders-async"
|
|
22
|
+
TOPUPS_SLUG = "orders/topups"
|
|
23
|
+
|
|
24
|
+
# Voucher endpoints
|
|
25
|
+
VOUCHERS_SLUG = "voucher/airmoney"
|
|
26
|
+
VOUCHERS_ESIM_SLUG = "voucher/esim"
|
|
27
|
+
|
|
28
|
+
# SIM endpoints
|
|
29
|
+
SIMS_SLUG = "sims"
|
|
30
|
+
SIMS_USAGE = "usage"
|
|
31
|
+
SIMS_TOPUPS = "topups"
|
|
32
|
+
SIMS_PACKAGES = "packages"
|
|
33
|
+
|
|
34
|
+
# Instructions and compatibility
|
|
35
|
+
INSTRUCTIONS_SLUG = "instructions"
|
|
36
|
+
COMPATIBILITY_SLUG = "compatible-devices-lite"
|
|
37
|
+
|
|
38
|
+
# Finance endpoints
|
|
39
|
+
EXCHANGE_RATES_SLUG = "finance/exchange-rates"
|
|
40
|
+
|
|
41
|
+
# Future orders
|
|
42
|
+
FUTURE_ORDERS = "future-orders"
|
|
43
|
+
CANCEL_FUTURE_ORDERS = "cancel-future-orders"
|
|
44
|
+
|
|
45
|
+
# Catalog
|
|
46
|
+
OVERRIDE_SLUG = "packages/overrides"
|
|
47
|
+
|
|
48
|
+
# Notifications
|
|
49
|
+
NOTIFICATIONS_SLUG = "notifications"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SDK Constants Module
|
|
3
|
+
|
|
4
|
+
This module contains SDK-specific constants such as version, limits, and timeouts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SdkConstants:
|
|
9
|
+
"""SDK-specific constants for Airalo SDK."""
|
|
10
|
+
|
|
11
|
+
# SDK Version
|
|
12
|
+
VERSION = "1.0.0"
|
|
13
|
+
|
|
14
|
+
# Order limits
|
|
15
|
+
BULK_ORDER_LIMIT = 50
|
|
16
|
+
ORDER_LIMIT = 50
|
|
17
|
+
FUTURE_ORDER_LIMIT = 50
|
|
18
|
+
|
|
19
|
+
# Voucher limits
|
|
20
|
+
VOUCHER_MAX_NUM = 100000
|
|
21
|
+
VOUCHER_MAX_QUANTITY = 100
|
|
22
|
+
|
|
23
|
+
# HTTP settings
|
|
24
|
+
DEFAULT_TIMEOUT = 60 # seconds
|
|
25
|
+
DEFAULT_RETRY_COUNT = 2
|
|
26
|
+
|
|
27
|
+
# Cache settings
|
|
28
|
+
DEFAULT_CACHE_TTL = 3600 # 1 hour in seconds
|
|
29
|
+
TOKEN_CACHE_TTL = 3600 # 1 hour in seconds
|
|
30
|
+
|
|
31
|
+
# Concurrency settings
|
|
32
|
+
MAX_CONCURRENT_REQUESTS = 5
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exceptions package for Airalo SDK.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .airalo_exception import (
|
|
6
|
+
AiraloException,
|
|
7
|
+
ConfigurationError,
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
ValidationError,
|
|
10
|
+
APIError,
|
|
11
|
+
NetworkError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AiraloException",
|
|
16
|
+
"ConfigurationError",
|
|
17
|
+
"AuthenticationError",
|
|
18
|
+
"ValidationError",
|
|
19
|
+
"APIError",
|
|
20
|
+
"NetworkError",
|
|
21
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Airalo Exception Module
|
|
3
|
+
|
|
4
|
+
This module defines custom exceptions for the Airalo SDK.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AiraloException(Exception):
|
|
9
|
+
"""
|
|
10
|
+
Base exception class for Airalo SDK.
|
|
11
|
+
|
|
12
|
+
This exception is raised when SDK-specific errors occur,
|
|
13
|
+
such as configuration errors, API errors, or validation errors.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, message: str, error_code: str = None, http_status: int = None):
|
|
17
|
+
"""
|
|
18
|
+
Initialize AiraloException.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
message: Error message describing the exception
|
|
22
|
+
error_code: Optional error code from API
|
|
23
|
+
http_status: Optional HTTP status code
|
|
24
|
+
"""
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
self.message = message
|
|
27
|
+
self.error_code = error_code
|
|
28
|
+
self.http_status = http_status
|
|
29
|
+
|
|
30
|
+
def __str__(self) -> str:
|
|
31
|
+
"""Return string representation of the exception."""
|
|
32
|
+
if self.error_code:
|
|
33
|
+
return f"[{self.error_code}] {self.message}"
|
|
34
|
+
return self.message
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ConfigurationError(AiraloException):
|
|
38
|
+
"""Exception raised for configuration-related errors."""
|
|
39
|
+
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AuthenticationError(AiraloException):
|
|
44
|
+
"""Exception raised for authentication failures."""
|
|
45
|
+
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ValidationError(AiraloException):
|
|
50
|
+
"""Exception raised for validation errors."""
|
|
51
|
+
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class APIError(AiraloException):
|
|
56
|
+
"""Exception raised for API-related errors."""
|
|
57
|
+
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class NetworkError(AiraloException):
|
|
62
|
+
"""Exception raised for network-related errors."""
|
|
63
|
+
|
|
64
|
+
pass
|
airalo/helpers/cached.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Caching Helper Module
|
|
3
|
+
|
|
4
|
+
This module provides file-based caching functionality for the SDK.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import os
|
|
9
|
+
import pickle
|
|
10
|
+
import tempfile
|
|
11
|
+
import time
|
|
12
|
+
from ..constants.sdk_constants import SdkConstants
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Callable, Optional, Union
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Cached:
|
|
18
|
+
"""
|
|
19
|
+
File-based caching utility.
|
|
20
|
+
|
|
21
|
+
Provides simple file-based caching with TTL support using
|
|
22
|
+
project root cache directory.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
CACHE_KEY_PREFIX = "airalo_"
|
|
26
|
+
|
|
27
|
+
_cache_path: Optional[Path] = None
|
|
28
|
+
_cache_name: str = ""
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def get(
|
|
32
|
+
cls, work: Union[Callable[[], Any], Any], cache_name: str, ttl: int = 0
|
|
33
|
+
) -> Any:
|
|
34
|
+
"""
|
|
35
|
+
Get cached value or compute and cache it.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
work: Callable that produces the value, or the value itself
|
|
39
|
+
cache_name: Unique name for this cache entry
|
|
40
|
+
ttl: Time-to-live in seconds (0 uses default)
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Cached or computed value
|
|
44
|
+
"""
|
|
45
|
+
cls._init(cache_name)
|
|
46
|
+
|
|
47
|
+
cache_id = cls._get_id(cache_name)
|
|
48
|
+
|
|
49
|
+
# Try to get from cache
|
|
50
|
+
cached_result = cls._cache_get(cache_id, ttl)
|
|
51
|
+
if cached_result is not None:
|
|
52
|
+
return cached_result
|
|
53
|
+
|
|
54
|
+
# Compute result
|
|
55
|
+
if callable(work):
|
|
56
|
+
result = work()
|
|
57
|
+
else:
|
|
58
|
+
result = work
|
|
59
|
+
|
|
60
|
+
# Cache and return
|
|
61
|
+
return cls._cache_this(cache_id, result)
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def clear_cache(cls) -> None:
|
|
65
|
+
"""Clear all cache files."""
|
|
66
|
+
cls._init()
|
|
67
|
+
|
|
68
|
+
# Find and remove all cache files
|
|
69
|
+
cache_pattern = cls._cache_path / f"{cls.CACHE_KEY_PREFIX}*"
|
|
70
|
+
for cache_file in cls._cache_path.glob(f"{cls.CACHE_KEY_PREFIX}*"):
|
|
71
|
+
try:
|
|
72
|
+
cache_file.unlink()
|
|
73
|
+
except OSError:
|
|
74
|
+
pass # Ignore errors when deleting cache files
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def _init(cls, cache_name: str = "") -> None:
|
|
78
|
+
"""
|
|
79
|
+
Initialize cache directory and name.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
cache_name: Optional cache name to set
|
|
83
|
+
"""
|
|
84
|
+
if cls._cache_path is None:
|
|
85
|
+
# Use project root cache directory
|
|
86
|
+
cls._cache_path = Path(__file__).resolve().parent.parent.parent / ".cache"
|
|
87
|
+
cls._cache_path.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
|
|
89
|
+
if cache_name:
|
|
90
|
+
cls._cache_name = cache_name
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def _get_id(cls, key: str) -> str:
|
|
94
|
+
"""
|
|
95
|
+
Generate cache file ID from key.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
key: Cache key
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Cache file ID
|
|
102
|
+
"""
|
|
103
|
+
return cls.CACHE_KEY_PREFIX + hashlib.md5(key.encode()).hexdigest()
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def _cache_get(cls, cache_id: str, custom_ttl: int = 0) -> Optional[Any]:
|
|
107
|
+
"""
|
|
108
|
+
Retrieve value from cache if valid.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
cache_id: Cache file ID
|
|
112
|
+
custom_ttl: Custom TTL in seconds
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Cached value or None if not found/expired
|
|
116
|
+
"""
|
|
117
|
+
cache_file = cls._cache_path / cache_id
|
|
118
|
+
|
|
119
|
+
if not cache_file.exists():
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
# Check TTL
|
|
123
|
+
now = time.time()
|
|
124
|
+
file_mtime = cache_file.stat().st_mtime
|
|
125
|
+
ttl = custom_ttl if custom_ttl > 0 else SdkConstants.DEFAULT_CACHE_TTL
|
|
126
|
+
|
|
127
|
+
if now - file_mtime > ttl:
|
|
128
|
+
# Cache expired, remove file
|
|
129
|
+
try:
|
|
130
|
+
cache_file.unlink()
|
|
131
|
+
except OSError:
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
# Read cached data
|
|
135
|
+
try:
|
|
136
|
+
with open(cache_file, "rb") as f:
|
|
137
|
+
return pickle.load(f)
|
|
138
|
+
except (OSError, pickle.PickleError):
|
|
139
|
+
# Corrupted cache file, remove it
|
|
140
|
+
try:
|
|
141
|
+
cache_file.unlink()
|
|
142
|
+
except OSError:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def _cache_this(cls, cache_id: str, result: Any) -> Any:
|
|
147
|
+
"""
|
|
148
|
+
Store value in cache.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
cache_id: Cache file ID
|
|
152
|
+
result: Value to cache
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
The cached value
|
|
156
|
+
"""
|
|
157
|
+
if result is None:
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
cache_file = cls._cache_path / cache_id
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
# Write cache file
|
|
164
|
+
with open(cache_file, "wb") as f:
|
|
165
|
+
pickle.dump(result, f)
|
|
166
|
+
|
|
167
|
+
# Try to set permissions (may fail on some systems)
|
|
168
|
+
try:
|
|
169
|
+
cache_file.chmod(0o666)
|
|
170
|
+
except OSError:
|
|
171
|
+
pass # Ignore permission errors
|
|
172
|
+
|
|
173
|
+
except (OSError, pickle.PickleError) as e:
|
|
174
|
+
# Failed to cache, but return the result anyway
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
return result
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cloud SIM Share Validator
|
|
3
|
+
|
|
4
|
+
Helper class for validating SIM cloud sharing data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any, Dict, List
|
|
10
|
+
|
|
11
|
+
from ..exceptions.airalo_exception import AiraloException
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CloudSimShareValidator:
|
|
15
|
+
"""
|
|
16
|
+
Validator for SIM cloud sharing payloads.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
_email_regex = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
|
|
20
|
+
_allowed_sharing_options = {"link", "pdf"}
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def validate(
|
|
24
|
+
sim_cloud_share: Dict[str, Any], required_fields: List[str] = None
|
|
25
|
+
) -> bool:
|
|
26
|
+
"""
|
|
27
|
+
Validate the SIM cloud sharing payload.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
sim_cloud_share: The payload dictionary.
|
|
31
|
+
required_fields: List of required fields to validate.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
AiraloException: If validation fails.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
True if valid.
|
|
38
|
+
"""
|
|
39
|
+
required_fields = required_fields or []
|
|
40
|
+
|
|
41
|
+
CloudSimShareValidator._check_required_fields(sim_cloud_share, required_fields)
|
|
42
|
+
|
|
43
|
+
# Validate 'to_email'
|
|
44
|
+
to_email = sim_cloud_share.get("to_email")
|
|
45
|
+
if to_email and not CloudSimShareValidator._email_regex.match(to_email):
|
|
46
|
+
raise AiraloException(
|
|
47
|
+
f"The to_email must be a valid email address, payload: {json.dumps(sim_cloud_share)}"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Validate 'sharing_option'
|
|
51
|
+
for option in sim_cloud_share.get("sharing_option", []):
|
|
52
|
+
if option not in CloudSimShareValidator._allowed_sharing_options:
|
|
53
|
+
allowed = " or ".join(CloudSimShareValidator._allowed_sharing_options)
|
|
54
|
+
raise AiraloException(
|
|
55
|
+
f"The sharing_option may be {allowed} or both, payload: {json.dumps(sim_cloud_share)}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Validate 'copy_address' emails
|
|
59
|
+
for cc_email in sim_cloud_share.get("copy_address", []):
|
|
60
|
+
if not CloudSimShareValidator._email_regex.match(cc_email):
|
|
61
|
+
raise AiraloException(
|
|
62
|
+
f"The copy_address: {cc_email} must be a valid email address, payload: {json.dumps(sim_cloud_share)}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def _check_required_fields(
|
|
69
|
+
sim_cloud_share: Dict[str, Any], required_fields: List[str]
|
|
70
|
+
) -> bool:
|
|
71
|
+
"""
|
|
72
|
+
Ensure required fields exist and are not empty.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
sim_cloud_share: Payload dictionary.
|
|
76
|
+
required_fields: List of required keys.
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
AiraloException: If a required field is missing or empty.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
True if all required fields are present.
|
|
83
|
+
"""
|
|
84
|
+
for field in required_fields:
|
|
85
|
+
if not sim_cloud_share.get(field):
|
|
86
|
+
raise AiraloException(
|
|
87
|
+
f"The {field} field is required, payload: {json.dumps(sim_cloud_share)}"
|
|
88
|
+
)
|
|
89
|
+
return True
|