Files
rengine/tools/httpx/client.py
T
Yogesh OjhaandClaude Opus 5 2cb8d73280 style: strip explanatory comments across the codebase
The comment rule in CLAUDE.md is one line maximum, never narration: no
multi-line blocks, no module-header explainers, no sentences that explain or
justify the code to the reader. The tree had drifted a long way from it, and
most of the drift is mine.

Removed:
  232  lines of multi-line comment blocks
  203  single-line comments that argued instead of stating
  154  comments joined by so/because/otherwise/rather-than
  143  narrating comments in the frontend
  112  test docstrings (the test name already says what it asserts)
   25  test module docstrings and 25 decorative section dividers

Collapsed:
  324  multi-line docstrings to one factual line
  271  one-line docstrings that had a justifying clause bolted on

Kept: functional directives, section dividers, short field labels, alembic
revision headers, and the eight FastAPI route docstrings that render in /docs.
Where a fact was load-bearing it is in CLAUDE.md, which is where it belongs.

284 python tests and 34 frontend tests pass, ruff and ruff-format clean across
ten packages, svelte-check 0 errors, prettier and eslint clean, app.main imports.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dKUV96WkQQETZBMoAG98C
2026-09-11 14:11:23 +05:30

156 lines
4.6 KiB
Python

from __future__ import annotations
import contextlib
from collections.abc import Iterator
from shared.logging import get_logger
from tools.runner import CLIToolRunner, StreamOutcome, ToolNotFoundError
from tools.runner.models import CommandRecorder
logger = get_logger(__name__)
HTTPX_BINARY = "httpx"
DEFAULT_TIMEOUT = 900
_IDLE_FLOOR = 120
_IDLE_TIMEOUT_FACTOR = 6
_CAPTURE_IDLE_FLOOR = 300
CAPTURE_SECONDS_PER_TARGET = 6
MAX_CAPTURE_SECONDS = 7200
_RESPONSE_SIZE_CAP = 131072 # 128 KiB
_ENRICH_FLAGS = [
"-status-code",
"-title",
"-tech-detect",
"-web-server",
"-content-length",
"-content-type",
"-location",
"-ip",
"-cname",
"-cdn",
"-tls-grab",
"-jarm",
"-favicon",
"-hash",
"sha256",
"-http2",
"-pipeline",
"-include-response",
"-body-preview",
"-response-size-to-read",
str(_RESPONSE_SIZE_CAP),
"-no-color",
]
class HttpxError(Exception):
pass
class HttpxClient:
def __init__(
self,
*,
rate_limit: int | None = None,
threads: int = 50,
timeout: int = 10,
proxy_url: str | None = None,
headers: dict[str, str] | None = None,
follow_redirects: bool = True,
store_dir: str | None = None,
recorder: CommandRecorder | None = None,
extra_args: list[str] | None = None,
) -> None:
self.rate_limit = rate_limit
self.threads = threads
self.timeout = timeout
self.proxy_url = proxy_url
self.headers = headers or {}
self.follow_redirects = follow_redirects
self.store_dir = store_dir
self.recorder = recorder
self.extra_args = extra_args or []
try:
self._runner = CLIToolRunner(HTTPX_BINARY, default_timeout=DEFAULT_TIMEOUT)
except ToolNotFoundError as e:
raise HttpxError(str(e)) from e
@contextlib.contextmanager
def stream_probe(self, targets: list[str]) -> Iterator[StreamOutcome]:
"""Probe targets, streaming parsed httpx records one at a time (memory-bounded)."""
if not targets:
yield StreamOutcome(records=iter(()), return_code=0)
return
args = list(_ENRICH_FLAGS)
if self.follow_redirects:
args.append("-follow-redirects")
if self.rate_limit:
args += ["-rate-limit", str(self.rate_limit)]
args += ["-threads", str(self.threads), "-timeout", str(self.timeout)]
if self.proxy_url:
args += ["-proxy", self.proxy_url]
for key, value in self.headers.items():
args += ["-header", f"{key}: {value}"]
with self._runner.stream_json(
args=args,
input_data=targets,
input_flag="-l",
json_flag="-json",
silent=True,
silent_flag="-silent",
timeout=0,
idle_timeout=max(_IDLE_FLOOR, self.timeout * _IDLE_TIMEOUT_FACTOR),
recorder=self.recorder,
tool=HTTPX_BINARY,
extra_args=self.extra_args,
) as stream:
yield stream
def _capture_args(self) -> list[str]:
args = [
"-status-code",
"-screenshot",
"-system-chrome",
"-exclude-screenshot-bytes",
"-no-color",
]
if self.store_dir:
args += ["-store-response-dir", self.store_dir]
if self.follow_redirects:
args.append("-follow-redirects")
args += ["-threads", str(self.threads), "-timeout", str(max(self.timeout, 20))]
if self.proxy_url:
args += ["-proxy", self.proxy_url]
for key, value in self.headers.items():
args += ["-header", f"{key}: {value}"]
return args
@contextlib.contextmanager
def stream_capture(self, targets: list[str]) -> Iterator[StreamOutcome]:
"""The same render, streaming each image as the browser finishes it."""
if not targets:
yield StreamOutcome(records=iter(()), return_code=0)
return
ceiling = min(
MAX_CAPTURE_SECONDS,
max(DEFAULT_TIMEOUT, len(targets) * CAPTURE_SECONDS_PER_TARGET),
)
with self._runner.stream_json(
args=self._capture_args(),
input_data=targets,
input_flag="-l",
json_flag="-json",
silent=True,
silent_flag="-silent",
timeout=ceiling,
idle_timeout=max(_CAPTURE_IDLE_FLOOR, self.timeout * _IDLE_TIMEOUT_FACTOR),
recorder=self.recorder,
tool=HTTPX_BINARY,
extra_args=self.extra_args,
) as stream:
yield stream