from hypertor import AsyncClient, TimeoutError, HypertorError
import asyncio
from dataclasses import dataclass
from typing import Optional, Any
import time
@dataclass
class ApiResponse:
success: bool
data: Optional[Any] = None
error: Optional[str] = None
latency_ms: float = 0.0
cached: bool = False
class SimpleCache:
def __init__(self, ttl_seconds: int = 60):
self._cache: dict = {}
self._ttl = ttl_seconds
def get(self, key: str) -> Optional[Any]:
if key in self._cache:
value, timestamp = self._cache[key]
if time.time() - timestamp < self._ttl:
return value
del self._cache[key]
return None
def set(self, key: str, value: Any):
self._cache[key] = (value, time.time())
class SimpleRateLimiter:
def __init__(self, requests_per_second: float = 10.0):
self._rate = requests_per_second
self._last_request = 0.0
async def acquire(self):
now = time.time()
min_interval = 1.0 / self._rate
elapsed = now - self._last_request
if elapsed < min_interval:
await asyncio.sleep(min_interval - elapsed)
self._last_request = time.time()
class SimpleCircuitBreaker:
def __init__(self, failure_threshold: int = 5, recovery_timeout: float = 30.0):
self._failure_count = 0
self._threshold = failure_threshold
self._recovery_timeout = recovery_timeout
self._last_failure_time = 0.0
self._is_open = False
def allow_request(self) -> bool:
if not self._is_open:
return True
if time.time() - self._last_failure_time > self._recovery_timeout:
self._is_open = False
self._failure_count = 0
return True
return False
def record_success(self):
self._failure_count = 0
self._is_open = False
def record_failure(self):
self._failure_count += 1
self._last_failure_time = time.time()
if self._failure_count >= self._threshold:
self._is_open = True
class SecureApiClient:
def __init__(
self,
base_url: str,
max_retries: int = 3,
requests_per_second: float = 10.0,
cache_ttl_seconds: int = 60,
timeout: int = 60
):
self.base_url = base_url.rstrip("/")
self.max_retries = max_retries
self.timeout = timeout
self._client: Optional[AsyncClient] = None
self._circuit_breaker = SimpleCircuitBreaker(
failure_threshold=5,
recovery_timeout=30.0
)
self._rate_limiter = SimpleRateLimiter(
requests_per_second=requests_per_second
)
self._cache = SimpleCache(ttl_seconds=cache_ttl_seconds)
self.stats = {
"requests": 0,
"successes": 0,
"failures": 0,
"cache_hits": 0,
"retries": 0
}
async def __aenter__(self):
self._client = await AsyncClient(timeout=self.timeout).__aenter__()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
if self._client:
await self._client.__aexit__(exc_type, exc_val, exc_tb)
def _get_retry_delay(self, attempt: int) -> float:
import random
base_delay = 1.0
max_delay = 30.0
delay = min(base_delay * (2 ** attempt), max_delay)
delay = delay * (0.5 + random.random())
return delay
async def get(
self,
endpoint: str,
use_cache: bool = True
) -> ApiResponse:
url = f"{self.base_url}/{endpoint.lstrip('/')}"
cache_key = f"GET:{url}"
if use_cache:
cached = self._cache.get(cache_key)
if cached is not None:
self.stats["cache_hits"] += 1
return ApiResponse(
success=True,
data=cached,
cached=True
)
if not self._circuit_breaker.allow_request():
return ApiResponse(
success=False,
error="Circuit breaker is open - service unavailable"
)
await self._rate_limiter.acquire()
self.stats["requests"] += 1
start_time = time.time()
last_error = None
for attempt in range(self.max_retries + 1):
try:
response = await self._client.get(url)
if response.status_code >= 500:
raise Exception(f"Server error: {response.status_code}")
if response.status_code >= 400:
return ApiResponse(
success=False,
error=f"Client error: {response.status_code}",
latency_ms=(time.time() - start_time) * 1000
)
data = response.json()
self._circuit_breaker.record_success()
self.stats["successes"] += 1
if use_cache:
self._cache.set(cache_key, data)
return ApiResponse(
success=True,
data=data,
latency_ms=(time.time() - start_time) * 1000
)
except TimeoutError:
last_error = "Request timed out"
self._circuit_breaker.record_failure()
except HypertorError as e:
last_error = str(e)
self._circuit_breaker.record_failure()
except Exception as e:
last_error = str(e)
self._circuit_breaker.record_failure()
if attempt < self.max_retries:
self.stats["retries"] += 1
delay = self._get_retry_delay(attempt)
print(f" ⚠️ Retry {attempt + 1}/{self.max_retries} after {delay:.1f}s: {last_error}")
await asyncio.sleep(delay)
self.stats["failures"] += 1
return ApiResponse(
success=False,
error=f"All retries failed: {last_error}",
latency_ms=(time.time() - start_time) * 1000
)
async def post(
self,
endpoint: str,
json_data: Optional[str] = None
) -> ApiResponse:
url = f"{self.base_url}/{endpoint.lstrip('/')}"
if not self._circuit_breaker.allow_request():
return ApiResponse(
success=False,
error="Circuit breaker is open"
)
await self._rate_limiter.acquire()
self.stats["requests"] += 1
start_time = time.time()
last_error = None
for attempt in range(self.max_retries + 1):
try:
response = await self._client.post(url, json=json_data)
if response.status_code >= 500:
raise Exception(f"Server error: {response.status_code}")
data = response.json()
self._circuit_breaker.record_success()
self.stats["successes"] += 1
return ApiResponse(
success=response.status_code < 400,
data=data,
error=None if response.status_code < 400 else f"Status: {response.status_code}",
latency_ms=(time.time() - start_time) * 1000
)
except TimeoutError:
last_error = "Request timed out"
self._circuit_breaker.record_failure()
except HypertorError as e:
last_error = str(e)
self._circuit_breaker.record_failure()
except Exception as e:
last_error = str(e)
self._circuit_breaker.record_failure()
if attempt < self.max_retries:
self.stats["retries"] += 1
delay = self._get_retry_delay(attempt)
await asyncio.sleep(delay)
self.stats["failures"] += 1
return ApiResponse(
success=False,
error=f"All retries failed: {last_error}",
latency_ms=(time.time() - start_time) * 1000
)
def print_stats(self):
total = self.stats["requests"]
if total == 0:
print(" No requests made")
return
success_rate = (self.stats["successes"] / total) * 100
cache_rate = (self.stats["cache_hits"] / (total + self.stats["cache_hits"])) * 100 if total else 0
print(f" 📊 Statistics:")
print(f" Total Requests: {total}")
print(f" Successes: {self.stats['successes']} ({success_rate:.1f}%)")
print(f" Failures: {self.stats['failures']}")
print(f" Cache Hits: {self.stats['cache_hits']} ({cache_rate:.1f}%)")
print(f" Retries: {self.stats['retries']}")
async def main():
print("🧅 hypertor - Secure API Client Example")
print("=" * 50)
print()
print("Note: Tor network requests can be slow. Please be patient.")
print()
try:
async with SecureApiClient(
base_url="https://httpbin.org",
max_retries=2,
requests_per_second=5.0,
cache_ttl_seconds=30,
timeout=60
) as client:
print("📡 Test 1: Basic GET request")
resp = await client.get("/ip")
if resp.success:
print(f" ✅ Your Tor IP: {resp.data}")
print(f" ⏱️ Latency: {resp.latency_ms:.0f}ms")
else:
print(f" ❌ Error: {resp.error}")
print("\n📡 Test 2: Cached request (same endpoint)")
resp = await client.get("/ip")
if resp.cached:
print(f" ✅ Cache hit! Instant response")
print(f" 📦 Cached data: {resp.data}")
else:
print(f" ⏱️ Latency: {resp.latency_ms:.0f}ms")
print("\n📡 Test 3: POST JSON data")
import json
json_body = json.dumps({
"secret_message": "Hello from Tor!",
"anonymous": True
})
resp = await client.post("/post", json_data=json_body)
if resp.success:
print(f" ✅ Response received")
print(f" ⏱️ Latency: {resp.latency_ms:.0f}ms")
else:
print(f" ❌ Error: {resp.error}")
print("\n" + "-" * 40)
client.print_stats()
print("\n✅ Secure API Client demo completed!")
except KeyboardInterrupt:
print("\n\n🛑 Interrupted by user")
except Exception as e:
print(f"\n❌ Error: {e}")
if __name__ == "__main__":
asyncio.run(main())