Extract shared proxy module; make it available to direct scraper; remove dead code

Proxy config, DataImpulse sticky-session username syntax, and Playwright/
httpx proxy builders now live in one place (services/proxy.py) instead of
being duplicated across the Booking.com backend and the /config/proxy test
endpoint. Both scrapers consume it.

- services/proxy.py: load_config/normalize (DB-authoritative, env fallback),
  new_session_id, username, playwright_proxy, httpx_proxy_url
- PlaywrightLocalBackend delegates proxy building to the module
- get_scraper_backend factory uses proxy.load_config (one resolution path)
- test_proxy_config endpoint uses the shared URL builder; httpx proxies= ->
  proxy= (forward-compatible, 0.28-safe)
- Direct booking-engine scraper (httpx) can now route through the same proxy,
  gated by the direct_scraper_use_proxy flag (default off, plumbing ready)

Dead code removed: set_scraper_paused (never called — rotate-on-block
replaced pause-on-block), get_competitor_matrix / get_hotels_list /
update_hotel_tier (endpoints have their own SQL), unused PROXY_KEYS tuple.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-05 22:55:24 +00:00
parent 6b7f00b40a
commit 1dc8c0a945
5 changed files with 147 additions and 194 deletions

View file

@ -7,7 +7,6 @@ No proxy - direct connection. Suitable for low-volume scraping.
import asyncio
import logging
import os
import random
import re
from datetime import date
@ -15,6 +14,8 @@ from decimal import Decimal, InvalidOperation
from typing import List, Optional
from urllib.parse import urlencode, urlparse, parse_qs
from services import proxy as proxy_util
def location_params_from_url(url: str) -> dict:
"""Pull the stable destination params (ss, dest_id, dest_type) out of a
@ -109,8 +110,9 @@ class PlaywrightLocalBackend(ScraperBackend):
Args:
proxy_config: Optional proxy configuration. Falls back to the
BOOKING_PROXY_* environment variables when not supplied.
See services/proxy.py for the shared config/build helpers.
"""
self.proxy_config = proxy_config if proxy_config is not None else self._proxy_from_env()
self.proxy_config = proxy_config if proxy_config is not None else proxy_util.config_from_env()
self._playwright = None
self._browser: Optional[Browser] = None
self._context: Optional[BrowserContext] = None
@ -122,40 +124,12 @@ class PlaywrightLocalBackend(ScraperBackend):
f"(country={self.proxy_config.get('country', 'gb')}, session={self._session_id})"
)
@staticmethod
def _proxy_from_env() -> dict:
"""Read proxy settings from BOOKING_PROXY_* env vars (empty = disabled)."""
host = os.getenv("BOOKING_PROXY_HOST", "").strip()
if not host:
return {}
return {
"host": host,
"port": os.getenv("BOOKING_PROXY_PORT", "823").strip(),
"username": os.getenv("BOOKING_PROXY_USERNAME", "").strip(),
"password": os.getenv("BOOKING_PROXY_PASSWORD", "").strip(),
"country": os.getenv("BOOKING_PROXY_COUNTRY", "gb").strip(),
}
def _proxy_enabled(self) -> bool:
return bool(self.proxy_config.get("host") and self.proxy_config.get("username"))
return proxy_util.is_enabled(self.proxy_config)
def _new_session_id(self):
"""Pick a fresh sticky-session id — DataImpulse pins one IP per id."""
self._session_id = f"hnf{random.randint(100000, 999999)}"
def _proxy_launch_arg(self) -> Optional[dict]:
"""Build Playwright's proxy dict, encoding country + sticky session
into the username per DataImpulse's syntax: LOGIN__cr.gb;sessid.ID."""
if not self._proxy_enabled():
return None
username = f"{self.proxy_config['username']}__cr.{self.proxy_config.get('country', 'gb')}"
if self._session_id:
username += f";sessid.{self._session_id}"
return {
"server": f"http://{self.proxy_config['host']}:{self.proxy_config['port']}",
"username": username,
"password": self.proxy_config["password"],
}
self._session_id = proxy_util.new_session_id()
async def rotate_session(self):
"""Burn the current proxy IP and warm cache; the next scrape gets a
@ -198,7 +172,7 @@ class PlaywrightLocalBackend(ScraperBackend):
# Proxy is set at launch (Chromium binds the sticky session, which
# lives in the username, at the network layer). Rotating the IP
# therefore relaunches the browser — see rotate_session().
proxy = self._proxy_launch_arg()
proxy = proxy_util.playwright_proxy(self.proxy_config, self._session_id)
if proxy:
launch_kwargs['proxy'] = proxy
self._browser = await self._playwright.chromium.launch(**launch_kwargs)