feat(proxy): add sticky-session proxy url support

This commit is contained in:
Anish Sarkar 2026-07-15 01:55:01 +05:30
parent 01fd6172ce
commit dafb0099c9
3 changed files with 41 additions and 6 deletions

View file

@ -15,6 +15,11 @@ def get_proxy_url() -> str | None:
return get_active_provider().get_proxy_url()
def get_sticky_proxy_url(session_id: str) -> str | None:
"""Proxy URL pinned to a stable vendor session when supported."""
return get_active_provider().get_sticky_proxy_url(session_id)
def get_playwright_proxy() -> dict[str, str] | None:
"""Playwright-style proxy dict, or ``None`` when not configured."""
return get_active_provider().get_playwright_proxy()
@ -45,5 +50,6 @@ __all__ = [
"get_proxy_url",
"get_requests_proxies",
"get_residential_proxy_url",
"get_sticky_proxy_url",
"is_pool_backed",
]

View file

@ -67,6 +67,14 @@ class ProxyProvider(ABC):
return None
return {"http": proxy_url, "https": proxy_url}
def get_sticky_proxy_url(self, session_id: str) -> str | None:
"""Return a proxy URL pinned to ``session_id`` when supported.
Providers without vendor-specific session routing safely fall back to
their ordinary proxy URL.
"""
return self.get_proxy_url()
def get_location(self) -> str:
"""Return the proxy's configured exit region (e.g. ``"us"``), or ``""``.

View file

@ -16,15 +16,13 @@ Example URL::
http://<token>__cr.us:<password>@gw.dataimpulse.com:823
ponytail: sticky sessions (a stable exit IP across requests) are another
username suffix (``__sid.<id>``) the lever the Reddit scraper's README flags as
a TODO for its ``loid``-per-IP flow. Not built yet: Reddit isn't wired to a
route, so there's no caller to thread a session id through. Add a
``get_sticky_proxy_url(session_id)`` here (rewriting the username) when it lands.
Sticky sessions use the vendor's ``__sid.<id>`` username suffix, allowing a
caller to keep cookies and requests on the same exit IP.
"""
import logging
from urllib.parse import urlsplit
import re
from urllib.parse import quote, urlsplit, urlunsplit
from app.config import Config
from app.utils.proxy.base import ProxyProvider
@ -34,6 +32,7 @@ logger = logging.getLogger(__name__)
# DataImpulse encodes country routing as a "__cr.<country>" username suffix; the
# country token runs until the next "__" param (e.g. "__sid") or the end.
_COUNTRY_MARKER = "__cr."
_SESSION_RE = re.compile(r"__sid\.[A-Za-z0-9_-]+")
class DataImpulseProvider(ProxyProvider):
@ -54,3 +53,25 @@ class DataImpulseProvider(ProxyProvider):
if _COUNTRY_MARKER not in username:
return ""
return username.split(_COUNTRY_MARKER, 1)[1].split("__", 1)[0].lower()
def get_sticky_proxy_url(self, session_id: str) -> str | None:
"""Return the configured URL with a deterministic sticky-session suffix."""
url = self.get_proxy_url()
if not url:
return None
safe_id = re.sub(r"[^A-Za-z0-9_-]", "-", session_id).strip("-")
if not safe_id:
raise ValueError("session_id must contain at least one letter or digit")
parts = urlsplit(url)
username = parts.username or ""
username = _SESSION_RE.sub("", username) + f"__sid.{safe_id}"
userinfo = quote(username, safe="%")
if parts.password is not None:
userinfo += f":{quote(parts.password, safe='%')}"
host = parts.hostname or ""
netloc = f"{userinfo}@{host}"
if parts.port:
netloc += f":{parts.port}"
return urlunsplit(
(parts.scheme, netloc, parts.path, parts.query, parts.fragment)
)