"""DECODO(구 Smartproxy) residential 프록시 제공자 — 쿠팡(Akamai) 전용. Decodo residential 은 **포트 기반 sticky** 모델이다: gate.decodo.com : 10001..N (각 포트 = 별도 sticky 세션, 대시보드에서 지속시간 지정: 예 10분) username/password 는 고정. → **IP 회전 = 포트를 바꾸는 것.** 매 요청 IP 변경은 Akamai 가 '쿠키-IP 불일치'로 재챌린지하므로 금물. → 시간창(now // window)으로 포트를 고른다: 창 안에선 같은 포트=같은 IP, 창이 지나면 다음 포트=새 IP. 자격증명/엔드포인트는 .env(DECODO_*) 에서 로드(시크릿). """ import os import time _UNSET = object() # 미지정(→env 사용) vs 명시적 빈값("" 등, 그대로 사용) 구분 def _int(v, default=None): try: return int(v) except (TypeError, ValueError): return default class DecodoProxy: def __init__(self, host=_UNSET, username=_UNSET, password=_UNSET, port_start=_UNSET, port_end=_UNSET, session_minutes=_UNSET): env = os.environ.get self.host = env("DECODO_HOST") if host is _UNSET else host self.username = env("DECODO_USERNAME") if username is _UNSET else username self.password = env("DECODO_PASSWORD") if password is _UNSET else password self.port_start = _int(env("DECODO_PORT_START") if port_start is _UNSET else port_start) self.port_end = _int(env("DECODO_PORT_END") if port_end is _UNSET else port_end) self.session_minutes = _int(env("DECODO_SESSION_MINUTES") if session_minutes is _UNSET else session_minutes, 10) @property def enabled(self) -> bool: return all([self.host, self.username, self.password, self.port_start, self.port_end]) def _port(self) -> int: """시간창 기반 포트 선택 — 창 안에선 동일 포트(동일 sticky IP), 창이 지나면 다음 포트.""" n = self.port_end - self.port_start + 1 bucket = int(time.time() // (self.session_minutes * 60)) return self.port_start + (bucket % n) def playwright_proxy(self) -> dict | None: """Playwright launch(proxy=...) 용 설정. 비활성 시 None(프록시 미사용).""" if not self.enabled: return None return { "server": f"http://{self.host}:{self._port()}", "username": self.username, "password": self.password, }