Python
Submit, poll, and retrieve with the Python standard library.
This complete async scrape example requires Python 3 and no additional packages. Set RUNHARVESTER_API_KEY in the process environment; the base URL defaults to https://api.runharvester.com/api/v1/data.
# Async scrape example. Requires Python 3; uses only the standard library.
# Set RUNHARVESTER_API_KEY before running. Keep the printed key and body for a manual retry.
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
import uuid
API_BASE_URL_ENV = "RUNHARVESTER_API_BASE_URL"
API_KEY_ENV = "RUNHARVESTER_API_KEY"
IDEMPOTENCY_KEY_ENV = "RUNHARVESTER_IDEMPOTENCY_KEY"
REQUEST_TIMEOUT_ENV = "RUNHARVESTER_REQUEST_TIMEOUT_MS"
POLL_INTERVAL_ENV = "RUNHARVESTER_POLL_INTERVAL_MS"
DEFAULT_API_BASE_URL = "https://api.runharvester.com/api/v1/data"
DEFAULT_REQUEST_TIMEOUT_MS = 120000
DEFAULT_POLL_INTERVAL_MS = 1000
HTTP_TIMEOUT_MS = 15000
CREATE_ATTEMPT_LIMIT = 2
RETRY_DELAY_MS = 250
RETRYABLE_STATUS_CODES = {502, 503, 504}
ACTIVE_STATUSES = {"pending", "running", "solving_captcha"}
api_base_url = os.environ.get(API_BASE_URL_ENV, DEFAULT_API_BASE_URL).rstrip("/")
api_key = os.environ.get(API_KEY_ENV)
if not api_key:
raise RuntimeError("Set RUNHARVESTER_API_KEY before running this async request example.")
idempotency_key = os.environ.get(IDEMPOTENCY_KEY_ENV) or str(uuid.uuid4())
request_body = json.dumps(
{"url": "https://example.com", "mode": "auto"}, separators=(",", ":")
).encode("utf-8")
print("Idempotency-Key:", idempotency_key)
request_timeout_ms = int(os.environ.get(REQUEST_TIMEOUT_ENV, DEFAULT_REQUEST_TIMEOUT_MS))
poll_interval_ms = int(os.environ.get(POLL_INTERVAL_ENV, DEFAULT_POLL_INTERVAL_MS))
if request_timeout_ms <= 0 or poll_interval_ms <= 0:
raise RuntimeError("Timeout and poll interval must be positive numbers in milliseconds.")
deadline = time.monotonic() + request_timeout_ms / 1000
def api_request(method, path, body=None):
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("Request exceeded its deadline.")
headers = {"Accept": "application/json", "X-API-Key": api_key}
if body is not None:
headers["Content-Type"] = "application/json"
headers["Idempotency-Key"] = idempotency_key
request = urllib.request.Request(api_base_url + path, data=body, headers=headers, method=method)
timeout = min(HTTP_TIMEOUT_MS / 1000, remaining)
try:
response = urllib.request.urlopen(request, timeout=timeout)
except urllib.error.HTTPError as error:
response = error
with response:
status_code = response.status
response_body = response.read()
try:
payload = json.loads(response_body)
except json.JSONDecodeError:
payload = response_body.decode("utf-8", errors="replace")
return status_code, payload
job_id = None
for attempt in range(1, CREATE_ATTEMPT_LIMIT + 1):
try:
status_code, payload = api_request("POST", "/requests", request_body)
except (TimeoutError, urllib.error.URLError) as error:
if attempt == CREATE_ATTEMPT_LIMIT:
raise RuntimeError("Could not create request after retrying with the same key and body.") from error
time.sleep(min(RETRY_DELAY_MS / 1000, max(0, deadline - time.monotonic())))
continue
if status_code in RETRYABLE_STATUS_CODES and attempt < CREATE_ATTEMPT_LIMIT:
time.sleep(min(RETRY_DELAY_MS / 1000, max(0, deadline - time.monotonic())))
continue
if status_code != 202:
raise RuntimeError("Create request failed with HTTP " + str(status_code) + ": " + str(payload))
job_id = payload.get("id")
if not job_id:
raise RuntimeError("Create response did not include a request id.")
break
if not job_id:
raise RuntimeError("Could not create request.")
while True:
status_code, status_payload = api_request("GET", "/requests/" + urllib.parse.quote(job_id, safe=""))
if status_code != 200:
raise RuntimeError("Status request failed with HTTP " + str(status_code) + ": " + str(status_payload))
status = status_payload.get("status")
if status == "completed":
break
if status in {"failed", "cancelled"}:
raise RuntimeError("Request ended with status " + status + ": " + str(status_payload.get("error", "")))
if status not in ACTIVE_STATUSES:
raise RuntimeError("Unknown request status: " + str(status))
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("Request exceeded its deadline.")
time.sleep(min(poll_interval_ms / 1000, remaining))
result_path = "/requests/" + urllib.parse.quote(job_id, safe="") + "/result"
status_code, result = api_request("GET", result_path)
if status_code != 200:
raise RuntimeError("Result request failed with HTTP " + str(status_code) + ": " + str(result))
print(json.dumps(result, indent=2))Keep this code on a trusted server or worker. For retry classification, see Retries and idempotency, and see the API reference for exact response fields.
Last updated on