""" Generated by the Lango developer portal. Do not edit by hand. Group: shop-kit (Shop Kit) APIs: Collect v1.2.0, SMS v1.4.1, Ledger v2.1.0 Generated: 2026-09-28T10:59:08.806Z """ from __future__ import annotations import base64 import json import time import urllib.error import urllib.parse import urllib.request from typing import Any, Optional class LangoApiError(Exception): """Raised for any non-2xx response. Carries status, error and requestId, same as the gateway.""" def __init__(self, status: int, error: str, message: str, request_id: Optional[str] = None): super().__init__(message) self.status = status self.error = error self.request_id = request_id class ShopKitClient: def __init__(self, consumer_key: str, consumer_secret: str, base_url: str): self.consumer_key = consumer_key self.consumer_secret = consumer_secret self.base_url = base_url.rstrip("/") self._token: Optional[str] = None self._token_expires_at: float = 0 def _get_token(self) -> str: if self._token and time.time() < self._token_expires_at: return self._token data = urllib.parse.urlencode({"grant_type": "client_credentials"}).encode() req = urllib.request.Request(f"{self.base_url}/oauth/v1/token", data=data, method="POST") basic = base64.b64encode(f"{self.consumer_key}:{self.consumer_secret}".encode()).decode() req.add_header("Authorization", f"Basic {basic}") req.add_header("Content-Type", "application/x-www-form-urlencoded") try: with urllib.request.urlopen(req) as res: body = json.loads(res.read()) except urllib.error.HTTPError as e: payload = json.loads(e.read() or b"{}") raise LangoApiError(e.code, payload.get("error", "TOKEN_ERROR"), payload.get("message", "Could not get a sandbox token."), payload.get("requestId")) self._token = body["access_token"] self._token_expires_at = time.time() + body.get("expires_in", 3600) - 60 return self._token def _call(self, method: str, path: str, query: Optional[dict] = None, body: Any = None) -> Any: token = self._get_token() items = {k: v for k, v in (query or {}).items() if v is not None} qs = f"?{urllib.parse.urlencode(items)}" if items else "" req = urllib.request.Request(f"{self.base_url}{path}{qs}", method=method) req.add_header("Authorization", f"Bearer {token}") if body is not None: req.data = json.dumps(body).encode() req.add_header("Content-Type", "application/json") try: with urllib.request.urlopen(req) as res: raw = res.read() return json.loads(raw) if raw else None except urllib.error.HTTPError as e: payload = json.loads(e.read() or b"{}") raise LangoApiError(e.code, payload.get("error", "REQUEST_FAILED"), payload.get("message", str(e)), payload.get("requestId")) def create_push(self, body: Optional[dict] = None) -> Any: """Send a payment prompt""" return self._call("POST", f"/collect/v1/push", query=None, body=body) def get_push(self, checkout_id: str) -> Any: """Check a payment prompt""" return self._call("GET", f"/collect/v1/push/{urllib.parse.quote(str(checkout_id))}", query=None, body=None) def send_sms(self, body: Optional[dict] = None) -> Any: """Send a text message""" return self._call("POST", f"/messaging/v1/sms", query=None, body=body) def get_sms(self, message_id: str) -> Any: """Check a message""" return self._call("GET", f"/messaging/v1/sms/{urllib.parse.quote(str(message_id))}", query=None, body=None) def get_transaction(self, transaction_id: str) -> Any: """Look up a transaction""" return self._call("GET", f"/ledger/v2/transactions/{urllib.parse.quote(str(transaction_id))}", query=None, body=None) def reverse_transaction(self, transaction_id: str, body: Optional[dict] = None) -> Any: """Reverse a transaction""" return self._call("POST", f"/ledger/v2/transactions/{urllib.parse.quote(str(transaction_id))}/reversal", query=None, body=body) def get_balance(self, short_code: Optional[str] = None) -> Any: """Read your balances""" return self._call("GET", f"/ledger/v2/balance", query={"shortCode": short_code}, body=None)