124 lines
4.3 KiB
Python
Executable File
124 lines
4.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Parallel API Key Rotator Proxy
|
|
Rotates Parallel API keys for search/extract/task/monitor endpoints.
|
|
Retries on 402 (no credit) and 429 (rate limit) with next key.
|
|
|
|
Usage:
|
|
python parallel_proxy.py --port 8765 --keys parallel_api_keys.txt
|
|
"""
|
|
|
|
import json, os, sys, time, threading
|
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
from urllib.parse import urlparse
|
|
import requests
|
|
|
|
PARALLEL_BASE = "https://api.parallel.ai"
|
|
|
|
class KeyPool:
|
|
def __init__(self, keys_file):
|
|
self.keys = []
|
|
self.lock = threading.Lock()
|
|
self.stats = {}
|
|
self.load_keys(keys_file)
|
|
|
|
def load_keys(self, keys_file):
|
|
with open(keys_file, 'r') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line or line.startswith('#'):
|
|
continue
|
|
key = line.split('#')[0].strip()
|
|
if key:
|
|
self.keys.append(key)
|
|
self.stats[key] = {'uses': 0, 'fails': 0, 'last_used': 0}
|
|
print(f"Loaded {len(self.keys)} keys")
|
|
|
|
def get_key(self):
|
|
with self.lock:
|
|
if not self.keys:
|
|
return None
|
|
key = min(self.keys, key=lambda k: self.stats[k]['uses'])
|
|
self.stats[key]['uses'] += 1
|
|
self.stats[key]['last_used'] = time.time()
|
|
return key
|
|
|
|
def mark_fail(self, key):
|
|
with self.lock:
|
|
if key in self.stats:
|
|
self.stats[key]['fails'] += 1
|
|
|
|
def get_stats(self):
|
|
with self.lock:
|
|
total_uses = sum(s['uses'] for s in self.stats.values())
|
|
total_fails = sum(s['fails'] for s in self.stats.values())
|
|
return {
|
|
'total_keys': len(self.keys),
|
|
'total_uses': total_uses,
|
|
'total_fails': total_fails,
|
|
}
|
|
|
|
class ParallelAPI:
|
|
def __init__(self, key_pool):
|
|
self.pool = key_pool
|
|
self.session = requests.Session()
|
|
|
|
def _call(self, endpoint, data, method='POST', retry=3):
|
|
for attempt in range(retry):
|
|
key = self.pool.get_key()
|
|
if not key:
|
|
return None, 503, {'error': 'No keys available'}
|
|
|
|
headers = {
|
|
'Authorization': f'Bearer {key}',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
url = f"{PARALLEL_BASE}{endpoint}"
|
|
|
|
try:
|
|
if method == 'POST':
|
|
r = self.session.post(url, headers=headers, json=data, timeout=30)
|
|
else:
|
|
r = self.session.get(url, headers=headers, params=data, timeout=30)
|
|
|
|
# 402/429 → try next key
|
|
if r.status_code in (402, 429):
|
|
self.pool.mark_fail(key)
|
|
continue
|
|
|
|
# 200 with credit error body → try next key
|
|
if r.status_code == 200:
|
|
try:
|
|
body = r.json()
|
|
if body.get('type') == 'error' and 'credit' in body.get('error', {}).get('message', '').lower():
|
|
self.pool.mark_fail(key)
|
|
continue
|
|
except:
|
|
pass
|
|
|
|
if r.status_code >= 400:
|
|
self.pool.mark_fail(key)
|
|
return r.json() if r.text else {}, r.status_code, r.text
|
|
|
|
return r.json(), r.status_code, None
|
|
except Exception as e:
|
|
self.pool.mark_fail(key)
|
|
continue
|
|
|
|
return None, 503, {'error': 'All keys exhausted'}
|
|
|
|
def search(self, queries, **kwargs):
|
|
data = {"search_queries": queries if isinstance(queries, list) else [queries]}
|
|
data.update(kwargs)
|
|
return self._call('/v1/search', data)
|
|
|
|
def extract(self, urls, **kwargs):
|
|
if isinstance(urls, str):
|
|
urls = [urls]
|
|
return self._call('/v1/extract', {"urls": urls, **kwargs})
|
|
|
|
def task(self, query, **kwargs):
|
|
return self._call('/v1/task', {"query": query, **kwargs})
|
|
|
|
def monitor(self, url, **kwargs):
|
|
return self._call('/v1/monitor', {"url": url, **kwargs}) |