"""Worker configuration, parsed once from the environment. All env knobs the workers honor live here so there's a single source of truth (the two market workers used to each re-parse the same ~15 vars). Frozen dataclass — read it, don't mutate it. """ import os from dataclasses import dataclass def _int(name: str, default: int) -> int: return int(os.environ.get(name, str(default))) def _float(name: str, default: float) -> float: return float(os.environ.get(name, str(default))) def _flag(name: str) -> bool: return os.environ.get(name) == "1" @dataclass(frozen=True) class Settings: # C2 c2_url: str token: str # Session / pacing market_url: str # "" => use the worker's own default page solve_seconds: int delay: float jitter: float idle_seconds: int # Browser browser_path: str | None load_images: bool chrome_no_sandbox: bool # Browser bring-up resilience. nodriver gives Chromium only ~2.75s to open its # DevTools port before raising "Failed to connect to browser"; when many replicas # cold-start at once on a CPU-bound host they blow that window. A randomized pre-launch # delay de-synchronizes the herd, and a few retries cover the residual slow starts. startup_jitter: float browser_start_retries: int browser_start_backoff: float # Proxy (auth-free fallback) proxy: str | None # IPRoyal residential gateway iproyal_host: str iproyal_port: int iproyal_username: str | None iproyal_password: str | None iproyal_country: str iproyal_lifetime_min: int # Logging log_level: str log_json: bool @property def use_iproyal(self) -> bool: """IPRoyal takes priority over a plain PROXY when its creds are set.""" return bool(self.iproyal_username and self.iproyal_password) @classmethod def from_env(cls) -> "Settings": return cls( c2_url=os.environ.get("C2_URL", "http://localhost:5080").rstrip("/"), token=os.environ.get("WORKER_TOKEN", "dev-worker-token"), market_url=os.environ.get("MARKET_URL", ""), solve_seconds=_int("SOLVE_SECONDS", 30), delay=_float("DELAY", 2.0), jitter=_float("JITTER", 1.5), idle_seconds=_int("IDLE_SECONDS", 10), browser_path=os.environ.get("BROWSER_PATH") or None, # Residential proxy is metered per GB; Cloudflare gates on JS, not images, and # the market APIs are pure JSON — so block images unless explicitly debugging. load_images=_flag("LOAD_IMAGES"), chrome_no_sandbox=_flag("CHROME_NO_SANDBOX"), startup_jitter=_float("STARTUP_JITTER", 8.0), browser_start_retries=_int("BROWSER_START_RETRIES", 4), browser_start_backoff=_float("BROWSER_START_BACKOFF", 2.0), proxy=os.environ.get("PROXY") or None, iproyal_host=os.environ.get("IPROYAL_HOST", "geo.iproyal.com"), iproyal_port=_int("IPROYAL_PORT", 12321), iproyal_username=os.environ.get("IPROYAL_USERNAME") or None, iproyal_password=os.environ.get("IPROYAL_PASSWORD") or None, iproyal_country=os.environ.get("IPROYAL_COUNTRY", "us").strip().lower(), iproyal_lifetime_min=_int("IPROYAL_LIFETIME_MIN", 60), log_level=os.environ.get("LOG_LEVEL", "INFO").upper(), log_json=_flag("LOG_JSON"), )