#!/usr/bin/env python3 """ Specto LCO API — Python 3 example client (standard library only). SPECTO_API_BASE=https://api.spectotv.com/api/v1 SPECTO_API_KEY=spk_... SPECTO_API_SECRET=sps_... python3 specto_client.py Keep the secret on your server only. """ import hashlib, hmac, json, os, secrets, time, urllib.error, urllib.parse, urllib.request def canonical_query(query: str) -> str: if not query: return "" pairs = [] for part in query.split("&"): if not part: continue k, _, v = part.partition("=") k = urllib.parse.unquote(k.replace("+", "%20")) v = urllib.parse.unquote(v.replace("+", "%20")) pairs.append((urllib.parse.quote(k, safe="-_.~"), urllib.parse.quote(v, safe="-_.~"))) pairs.sort() return "&".join(f"{k}={v}" for k, v in pairs) def sign(secret: str, method: str, path: str, query: str, ts: str, nonce: str, body: bytes) -> str: sts = "\n".join([method.upper(), path, canonical_query(query), ts, nonce, hashlib.sha256(body).hexdigest()]) return hmac.new(secret.encode(), sts.encode(), hashlib.sha256).hexdigest() class SpectoClient: def __init__(self, base: str, key: str, secret: str): self.base = base.rstrip("/") self.origin = self.base[: -len("/api/v1")] if self.base.endswith("/api/v1") else self.base self.key, self.secret = key, secret def request(self, method, endpoint, query=None, body=None, idempotency_key=None): path = "/api/v1/" + endpoint.lstrip("/") qs = urllib.parse.urlencode(query or {}, quote_via=urllib.parse.quote) raw = b"" if body is None else json.dumps(body, separators=(",", ":"), ensure_ascii=False).encode() ts, nonce = str(int(time.time())), secrets.token_hex(16) headers = { "X-API-Key": self.key, "X-Timestamp": ts, "X-Nonce": nonce, "X-Signature": sign(self.secret, method, path, qs, ts, nonce, raw), "Accept": "application/json", } if method == "POST": headers["Content-Type"] = "application/json" if idempotency_key: headers["Idempotency-Key"] = idempotency_key # optional extra headers, e.g. for your own outbound proxy (JSON array of "Name: value") for h in json.loads(os.environ.get("SPECTO_EXTRA_HEADERS", "[]")): k, _, v = h.partition(":"); headers[k.strip()] = v.strip() url = self.origin + path + ("?" + qs if qs else "") req = urllib.request.Request(url, data=raw if method == "POST" else None, headers=headers, method=method) try: with urllib.request.urlopen(req, timeout=90) as r: return r.status, json.loads(r.read()) except urllib.error.HTTPError as e: # 4xx/5xx still carry the JSON envelope return e.code, json.loads(e.read() or b"{}") if __name__ == "__main__": # Demo: balance -> plans -> (optional) renewal with a retry-safe idempotency key -> lookup. c = SpectoClient(os.environ.get("SPECTO_API_BASE", "https://api.spectotv.com/api/v1"), os.environ.get("SPECTO_API_KEY", ""), os.environ.get("SPECTO_API_SECRET", "")) status, bal = c.request("GET", "balance") print("balance:", status, bal.get("code"), bal.get("data", {}).get("wallet_balance")) status, plans = c.request("GET", "plans") plan_list = plans.get("data", {}).get("plans", []) print("plans:", len(plan_list)) subscriber = os.environ.get("SPECTO_DEMO_SUBSCRIBER") if subscriber and plan_list: key = "renew-" + secrets.token_hex(8) # store with your order before sending result = None for attempt in range(1, 4): try: result = c.request("POST", "renew", body={"subscriber_id": subscriber, "plan_id": plan_list[0]["id"]}, idempotency_key=key) break except urllib.error.URLError: # network problem: retry with the SAME key time.sleep(attempt) status, body = result print("renew:", status, body.get("code"), body.get("message")) if body.get("code") == "OK": d = body["data"] print(" expiry", d["expiry"]["previous"], "->", d["expiry"]["new"], "| wallet", d["wallet"]["before"], "->", d["wallet"]["after"]) print("lookup:", c.request("GET", "transaction", {"idempotency_key": key})[1].get("data", {}).get("status"))