"""SMScash API 0.22.0. Python 3.9+; standard library, no automatic retries.""" import json import re import urllib.error import urllib.parse import urllib.request class SmScashError(Exception): def __init__(self, error, status, request_id=None): super().__init__(error.get("message", "SMScash API request failed")) self.code = error.get("code", "HTTP_ERROR") self.status = status self.request_id = error.get("request_id") or request_id self.hint = error.get("hint") self.details = error.get("details") self.retryable = error.get("retryable") is True self.retry_after_seconds = error.get("retry_after_seconds") class _NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None def _positive(value): return type(value) is int and value > 0 def _validate_operator(parameters): if "provider_id" in parameters: raise ValueError("Use operator instead of provider_id.") if "operator" not in parameters: return value = parameters["operator"] if type(value) is int and 0 <= value <= 2147483647: return if isinstance(value, str): value = value.strip() if value.lower() in ("", "any", "0") or re.fullmatch(r"[1-9][0-9]*", value) and int(value) <= 2147483647: return raise ValueError("operator must be a public provider ID, or any/0 for automatic selection.") def _operation_key(value): if not isinstance(value, str) or not re.fullmatch(r"[\x21-\x7e]{8,128}", value): raise ValueError("Pass a unique Idempotency-Key with 8–128 ASCII characters; keep it for retries of the same operation.") return value class SmScash: def __init__(self, api_key, base_url="https://smscash.org", timeout=15): if not isinstance(api_key, str) or not api_key or re.search(r"\s", api_key): raise ValueError("A nonempty API key is required.") url = urllib.parse.urlsplit(base_url) if url.scheme != "https" or not url.hostname or url.username or url.password or url.query or url.fragment or url.path not in ("", "/"): raise ValueError("base_url must be an HTTPS origin.") if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or timeout <= 0: raise ValueError("timeout must be positive.") self._api_key = api_key self._base_url = base_url.rstrip("/") self._timeout = timeout self._opener = urllib.request.build_opener(_NoRedirect) def _request(self, path, method="GET", query=None, body=None, request_key=None): url = self._base_url + path if query: url += "?" + urllib.parse.urlencode({key: value for key, value in query.items() if value is not None and value != ""}) headers = {"Authorization": "Bearer " + self._api_key, "User-Agent": "SMScash-Python/0.22.0"} payload = None if body is not None: headers["Content-Type"] = "application/json" payload = json.dumps(body, ensure_ascii=False).encode("utf-8") if request_key is not None: headers["Idempotency-Key"] = _operation_key(request_key) request = urllib.request.Request(url, data=payload, headers=headers, method=method) try: with self._opener.open(request, timeout=self._timeout) as response: try: result = json.load(response) if not isinstance(result, dict): raise ValueError("Expected an object") return result except (ValueError, UnicodeDecodeError): raise SmScashError({"code": "INVALID_RESPONSE", "message": "Expected JSON. Check the operation status before another write."}, response.status, response.headers.get("X-Request-ID")) from None except urllib.error.HTTPError as failure: with failure: try: result = json.load(failure) if not isinstance(result, dict): raise ValueError("Expected an object") except (ValueError, UnicodeDecodeError): result = {"error": {"code": "INVALID_RESPONSE", "message": "Expected JSON. Check the operation status before another write."}} raise SmScashError(result.get("error", {}), failure.code, failure.headers.get("X-Request-ID")) from None def me(self): return self._request("/api/v1/me") def balance(self): return self._request("/api/v1/summary") def hold_balance(self): return self._request("/api/v1/balance")["held_balance"] def catalog(self, **filters): _validate_operator(filters) return self._request("/api/v1/catalog", query=filters) def create_number(self, parameters, request_key): _operation_key(request_key) if not isinstance(parameters, dict): raise ValueError("Pass service + country, or offer_id.") _validate_operator(parameters) if "offer_id" in parameters: if not _positive(parameters["offer_id"]) or "service" in parameters or "country" in parameters: raise ValueError("Choose offer_id or service + country, not both.") elif not isinstance(parameters.get("service"), str) or not parameters["service"] or not _positive(parameters.get("country")): raise ValueError("service must be a catalog code; country must be a positive integer.") return self._request("/api/v1/activations", method="POST", body=parameters, request_key=request_key) def activation(self, session_id): if not _positive(session_id): raise ValueError("Session ID must be a positive integer.") return self._request("/api/v1/activations/" + str(session_id)) def sms(self, session_id, after_id=0, limit=100): if not _positive(session_id) or type(after_id) is not int or after_id < 0 or not _positive(limit) or limit > 200: raise ValueError("Use a positive session ID, after_id >= 0 and limit 1–200.") return self._request("/api/v1/activations/" + str(session_id) + "/sms", query={"after_id": after_id, "limit": limit}) def batch(self, ids): if not isinstance(ids, (list, tuple)) or not ids or len(ids) > 100 or any(not _positive(value) for value in ids): raise ValueError("Pass 1–100 positive session IDs.") return self._request("/api/v1/activations/batch", query={"ids": ",".join(map(str, ids))}) def set_status(self, session_id, status, request_key=None): if not _positive(session_id) or status not in ("retry", "finish", "cancel"): raise ValueError("Use a positive session ID and retry, finish or cancel.") if status == "retry": _operation_key(request_key) return self._request("/api/v1/activations/" + str(session_id) + "/" + status, method="POST", request_key=request_key) # Poll no more often than every 20 seconds; keep codes as strings and a cursor per session. # Never automatically retry ALLOCATION_UNCERTAIN or any write after a transport error.