コンテンツにスキップ

API Overview

httptap provides a clean Python API for programmatic use and extension. This page maps the public surface exported from the httptap package root; the linked pages drill into the core classes and the extensibility protocols.

Architecture

httptap is built around a modular architecture with clear, injectable interfaces:

┌─────────────────┐
│  CLI / Renderer │  wires Visualizers & Exporters
└────────┬────────┘
┌─────────────────┐
│  HTTPTapAnalyzer│  ◄── main entry point
└────────┬────────┘
         │  (injectable collaborators)
         ├─► DNS Resolver     (Protocol)
         ├─► TLS Inspector    (Protocol)
         ├─► Timing Collector (Protocol)
         └─► Request Executor (Protocol)

Main entry point

from httptap import HTTPTapAnalyzer

analyzer = HTTPTapAnalyzer()
steps = analyzer.analyze_url("https://httpbin.io")

analyze_url also accepts keyword-only arguments for non-GET requests:

from httptap import HTTPTapAnalyzer
from httptap.constants import HTTPMethod

analyzer = HTTPTapAnalyzer(follow_redirects=True, timeout=10.0)
steps = analyzer.analyze_url(
    "https://httpbin.io/post",
    method=HTTPMethod.POST,
    content=b'{"name": "John"}',
    headers={"Content-Type": "application/json"},
)

HTTPTapAnalyzer

HTTPTapAnalyzer(
    *,
    follow_redirects: bool = False,
    timeout: float = DEFAULT_TIMEOUT_SECONDS,
    http2: bool = True,
    verify_ssl: bool = True,
    ca_bundle_path: str | None = None,
    max_redirects: int = 10,
    request_executor: RequestExecutor | None = None,
    proxy: ProxyTypes | None = None,
    noproxy: bool = False,
    dns_resolver: DNSResolver | None = None,
    tls_inspector: TLSInspector | None = None,
    timing_collector_factory: type[TimingCollector]
    | None = None,
)

Orchestrates HTTP request analysis with redirect following.

This class manages the high-level flow of analyzing HTTP requests, including following redirect chains and collecting metrics at each step.

属性:

  • follow_redirects

    Whether to follow HTTP redirects.

  • timeout

    Request timeout in seconds.

  • http2

    Whether to enable HTTP/2 support.

  • max_redirects

    Maximum number of redirects to follow.

参数:

  • follow_redirects (bool, 默认: False ) –

    Whether to follow 3xx redirects.

  • timeout (float, 默认: DEFAULT_TIMEOUT_SECONDS ) –

    Request timeout in seconds.

  • http2 (bool, 默认: True ) –

    Enable HTTP/2 support.

  • verify_ssl (bool, 默认: True ) –

    Whether to verify TLS certificates.

  • ca_bundle_path (str | None, 默认: None ) –

    Path to custom CA certificate bundle (PEM format). Only used when verify_ssl is True. If None, uses system CA bundle.

  • max_redirects (int, 默认: 10 ) –

    Maximum number of redirects to follow.

  • request_executor (RequestExecutor | None, 默认: None ) –

    Object responsible for performing HTTP requests. Must implement the RequestExecutor protocol. Defaults to the built-in httpx implementation.

  • proxy (ProxyTypes | None, 默认: None ) –

    Optional proxy URL (http/https/socks5/socks5h) applied to all requests in the analysis chain.

  • noproxy (bool, 默认: False ) –

    When True, ignore proxy environment variables and connect directly. Triggered by --proxy "".

  • dns_resolver (DNSResolver | None, 默认: None ) –

    Custom DNS resolver implementation. If None, make_request will use its default (SystemDNSResolver).

  • tls_inspector (TLSInspector | None, 默认: None ) –

    Custom TLS inspector implementation. If None, make_request will use its default (SocketTLSInspector).

  • timing_collector_factory (type[TimingCollector] | None, 默认: None ) –

    Factory class for creating timing collectors. If None, make_request will use its default (PerfCounterTimingCollector). Note: This should be a class, not an instance, as a new collector is created for each request.

源代码位于: httptap/analyzer.py
def __init__(  # noqa: PLR0913
    self,
    *,
    follow_redirects: bool = False,
    timeout: float = DEFAULT_TIMEOUT_SECONDS,
    http2: bool = True,
    verify_ssl: bool = True,
    ca_bundle_path: str | None = None,
    max_redirects: int = 10,
    request_executor: RequestExecutor | None = None,
    proxy: ProxyTypes | None = None,
    noproxy: bool = False,
    dns_resolver: DNSResolver | None = None,
    tls_inspector: TLSInspector | None = None,
    timing_collector_factory: type[TimingCollector] | None = None,
) -> None:
    """Initialize HTTP analyzer.

    Args:
        follow_redirects: Whether to follow 3xx redirects.
        timeout: Request timeout in seconds.
        http2: Enable HTTP/2 support.
        verify_ssl: Whether to verify TLS certificates.
        ca_bundle_path: Path to custom CA certificate bundle (PEM format).
            Only used when verify_ssl is True. If None, uses system CA bundle.
        max_redirects: Maximum number of redirects to follow.
        request_executor: Object responsible for performing HTTP requests.
            Must implement the RequestExecutor protocol. Defaults to the
            built-in httpx implementation.
        proxy: Optional proxy URL (http/https/socks5/socks5h) applied to all
            requests in the analysis chain.
        noproxy: When True, ignore proxy environment variables and connect
            directly. Triggered by --proxy "".
        dns_resolver: Custom DNS resolver implementation. If None, make_request
            will use its default (SystemDNSResolver).
        tls_inspector: Custom TLS inspector implementation. If None, make_request
            will use its default (SocketTLSInspector).
        timing_collector_factory: Factory class for creating timing collectors.
            If None, make_request will use its default (PerfCounterTimingCollector).
            Note: This should be a class, not an instance, as a new collector
            is created for each request.

    """
    self.follow_redirects = follow_redirects
    self.timeout = timeout
    self.http2 = http2
    self.verify_ssl = verify_ssl
    self.ca_bundle_path = ca_bundle_path
    self.max_redirects = max_redirects
    self._request = request_executor or HTTPClientRequestExecutor()
    self._dns_resolver = dns_resolver
    self._tls_inspector = tls_inspector
    self._timing_collector = timing_collector_factory
    self._proxy = proxy
    self._noproxy = noproxy

See Core Components for the full method and data-model reference.

Core data model

A single request/response cycle is captured by StepMetrics, which nests timing, network, and response detail. Full field-level docs live on the Core Components page.

Protocol interfaces

httptap uses Protocol classes (PEP 544) for type-safe, inheritance-free extensibility. Each protocol and a worked custom implementation is documented on the Protocol Interfaces page: DNSResolver, TLSInspector, TimingCollector, Visualizer, Exporter, and RequestExecutor.

Request executor

For fully customised HTTP behaviour, implement the RequestExecutor protocol and pass an instance as request_executor= to HTTPTapAnalyzer. The protocol contract and its RequestOptions / RequestOutcome data classes are documented on the Protocol Interfaces page.

Built-in implementations

httptap ships production-ready defaults for every protocol; all are importable from the package root.

SystemDNSResolver

DNS resolver using the system getaddrinfo implementation.

Resolves hostnames with :func:socket.getaddrinfo on a background thread so the lookup can be bounded by a timeout. It implements the :class:~httptap.interfaces.DNSResolver protocol and is the default resolver used by the analyzer.

方法:

  • resolve

    Resolve host and return IP, family label, and elapsed milliseconds.

resolve

resolve(
    host: str, port: int, timeout: float
) -> tuple[str, str, float]

Resolve host and return IP, family label, and elapsed milliseconds.

参数:

  • host (str) –

    Hostname to resolve (e.g., "example.com").

  • port (int) –

    Port number used to hint the address lookup.

  • timeout (float) –

    Maximum time to wait for resolution, in seconds.

返回:

  • str

    Tuple of (ip_address, ip_family, elapsed_ms) where ip_family

  • str

    is one of "IPv4", "IPv6", or "AF_<num>" for other

  • float

    address families, and elapsed_ms is the resolution time in

  • tuple[str, str, float]

    milliseconds.

引发:

  • DNSResolutionError

    If resolution times out, the lookup fails, or no usable address record is returned.

源代码位于: httptap/implementations/dns.py
def resolve(self, host: str, port: int, timeout: float) -> tuple[str, str, float]:
    """Resolve host and return IP, family label, and elapsed milliseconds.

    Args:
        host: Hostname to resolve (e.g., "example.com").
        port: Port number used to hint the address lookup.
        timeout: Maximum time to wait for resolution, in seconds.

    Returns:
        Tuple of ``(ip_address, ip_family, elapsed_ms)`` where ``ip_family``
        is one of ``"IPv4"``, ``"IPv6"``, or ``"AF_<num>"`` for other
        address families, and ``elapsed_ms`` is the resolution time in
        milliseconds.

    Raises:
        DNSResolutionError: If resolution times out, the lookup fails, or no
            usable address record is returned.
    """
    start_time = time.perf_counter()

    addr_info: list[AddrInfo] | None = None
    worker_error: Exception | None = None

    def resolver_task() -> None:
        nonlocal addr_info, worker_error
        try:
            addr_info = cast(
                "list[AddrInfo]",
                socket.getaddrinfo(
                    host,
                    port,
                    family=socket.AF_UNSPEC,
                    type=socket.SOCK_STREAM,
                ),
            )
        except Exception as exc:  # pragma: no cover - handled below  # noqa: BLE001
            worker_error = exc

    thread = threading.Thread(target=resolver_task, daemon=True)
    thread.start()
    thread.join(timeout)

    if thread.is_alive():
        message = f"DNS resolution timed out for {host} after {timeout:.2f}s"
        raise DNSResolutionError(message)

    if worker_error:
        if isinstance(worker_error, socket.gaierror):
            message = f"DNS resolution failed for {host}: {worker_error}"
            raise DNSResolutionError(message) from worker_error
        details = f"Unexpected error during DNS resolution for {host}: {worker_error}"
        raise DNSResolutionError(details) from worker_error

    if addr_info is None:
        message = f"No address records for {host}"
        raise DNSResolutionError(message)

    if not addr_info:
        message = f"No address records for {host}"
        raise DNSResolutionError(message)

    records = _normalize_addrinfo(addr_info)
    if not records:
        message = f"No address records for {host}"
        raise DNSResolutionError(message)

    record = records[0]
    ip = str(record.sockaddr[0]) if record.sockaddr else None
    if not ip:
        message = f"Failed to extract IP address for {host}"
        raise DNSResolutionError(message)

    elapsed_ms = (time.perf_counter() - start_time) * MS_IN_SECOND
    ip_family = self._family_to_label(record.family)
    return ip, ip_family, elapsed_ms

SocketTLSInspector

SocketTLSInspector(
    *,
    verify: bool = True,
    ca_bundle_path: str | None = None,
)

TLS inspector that performs a dedicated TLS handshake using ssl.

Opens a short-lived TCP connection, performs a TLS handshake, and extracts the negotiated version, cipher suite, and leaf-certificate details into a :class:~httptap.models.NetworkInfo. Implements the :class:~httptap.interfaces.TLSInspector protocol and is the default inspector used by the analyzer.

参数:

  • verify (bool, 默认: True ) –

    Whether to verify TLS certificates.

  • ca_bundle_path (str | None, 默认: None ) –

    Path to custom CA certificate bundle (PEM format). Only used when verify is True. If None, uses system CA bundle.

方法:

  • inspect

    Inspect TLS connection and extract metadata.

源代码位于: httptap/implementations/tls.py
def __init__(self, *, verify: bool = True, ca_bundle_path: str | None = None) -> None:
    """Initialize inspector with optional verification toggle and custom CA bundle.

    Args:
        verify: Whether to verify TLS certificates.
        ca_bundle_path: Path to custom CA certificate bundle (PEM format).
            Only used when verify is True. If None, uses system CA bundle.

    """
    self._verify = verify
    self._ca_bundle_path = ca_bundle_path

inspect

inspect(
    host: str, port: int, timeout: float
) -> NetworkInfo

Inspect TLS connection and extract metadata.

参数:

  • host (str) –

    Hostname to connect to, also used for SNI.

  • port (int) –

    Port number (typically 443 for HTTPS).

  • timeout (float) –

    Connection timeout in seconds. The probe is additionally capped by TLS_PROBE_MAX_TIMEOUT_SECONDS.

返回:

  • NetworkInfo

    A NetworkInfo populated with the resolved IP, negotiated TLS

  • NetworkInfo

    version and cipher, and leaf-certificate details when available.

引发:

  • TLSInspectionError

    If the connection or TLS handshake fails.

源代码位于: httptap/implementations/tls.py
def inspect(self, host: str, port: int, timeout: float) -> NetworkInfo:
    """Inspect TLS connection and extract metadata.

    Args:
        host: Hostname to connect to, also used for SNI.
        port: Port number (typically 443 for HTTPS).
        timeout: Connection timeout in seconds. The probe is additionally
            capped by ``TLS_PROBE_MAX_TIMEOUT_SECONDS``.

    Returns:
        A NetworkInfo populated with the resolved IP, negotiated TLS
        version and cipher, and leaf-certificate details when available.

    Raises:
        TLSInspectionError: If the connection or TLS handshake fails.
    """
    network_info = NetworkInfo()
    network_info.tls_verified = self._verify
    probe_timeout = min(timeout, TLS_PROBE_MAX_TIMEOUT_SECONDS)

    try:
        connection = socket.create_connection((host, port), timeout=probe_timeout)
        with closing(connection) as raw_sock:
            self._populate_network_info(raw_sock, network_info)

            # Diagnostic tool: intentionally allows TLSv1.0+ to inspect legacy servers.
            # This is NOT a security issue because httptap is used for troubleshooting,
            # not for transmitting sensitive data in production.
            context = create_ssl_context(verify_ssl=self._verify, ca_bundle_path=self._ca_bundle_path)
            with context.wrap_socket(raw_sock, server_hostname=host) as tls_sock:
                tls_version, cipher_suite, cert_info = extract_tls_info(tls_sock)
                network_info.tls_version = tls_version
                network_info.tls_cipher = cipher_suite

                if cert_info:
                    apply_certificate_info(network_info, cert_info)

    except Exception as exc:
        msg = f"TLS inspection failed for {host}:{port}: {exc}"
        raise TLSInspectionError(
            msg,
        ) from exc

    return network_info

PerfCounterTimingCollector

PerfCounterTimingCollector()

High-precision timing collector using time.perf_counter().

Records monotonic timestamps as each request phase is marked and derives DNS, TTFB, and total durations from them. Implements the :class:~httptap.interfaces.TimingCollector protocol and is the default collector used by the analyzer. A fresh instance is created per request.

方法:

源代码位于: httptap/implementations/timing.py
def __init__(self) -> None:
    """Initialize timing collector with zeroed timestamps."""
    self._start_time = time.perf_counter()
    self._dns_start = 0.0
    self._dns_end = 0.0
    self._request_start = 0.0
    self._ttfb_time = 0.0
    self._end_time = 0.0

mark_dns_start

mark_dns_start() -> None

Record the beginning of DNS resolution.

源代码位于: httptap/implementations/timing.py
def mark_dns_start(self) -> None:
    """Record the beginning of DNS resolution."""
    self._dns_start = time.perf_counter()

mark_dns_end

mark_dns_end() -> None

Record the completion of DNS resolution.

源代码位于: httptap/implementations/timing.py
def mark_dns_end(self) -> None:
    """Record the completion of DNS resolution."""
    self._dns_end = time.perf_counter()

mark_request_start

mark_request_start() -> None

Record the moment the HTTP request starts.

源代码位于: httptap/implementations/timing.py
def mark_request_start(self) -> None:
    """Record the moment the HTTP request starts."""
    self._request_start = time.perf_counter()

mark_ttfb

mark_ttfb() -> None

Record when the first response byte is received.

源代码位于: httptap/implementations/timing.py
def mark_ttfb(self) -> None:
    """Record when the first response byte is received."""
    self._ttfb_time = time.perf_counter()

mark_request_end

mark_request_end() -> None

Record when the response body transfer completes.

源代码位于: httptap/implementations/timing.py
def mark_request_end(self) -> None:
    """Record when the response body transfer completes."""
    self._end_time = time.perf_counter()

get_metrics

get_metrics() -> TimingMetrics

Build a TimingMetrics instance populated with collected timings.

返回:

  • TimingMetrics

    A TimingMetrics with dns_ms, ttfb_ms, and total_ms

  • TimingMetrics

    computed from the recorded marks. Call calculate_derived() on

  • TimingMetrics

    the result to populate wait_ms and xfer_ms.

源代码位于: httptap/implementations/timing.py
def get_metrics(self) -> TimingMetrics:
    """Build a TimingMetrics instance populated with collected timings.

    Returns:
        A TimingMetrics with ``dns_ms``, ``ttfb_ms``, and ``total_ms``
        computed from the recorded marks. Call ``calculate_derived()`` on
        the result to populate ``wait_ms`` and ``xfer_ms``.
    """
    timing = TimingMetrics()
    timing.dns_ms = (self._dns_end - self._dns_start) * MS_IN_SECOND
    timing.total_ms = (self._end_time - self._start_time) * MS_IN_SECOND
    timing.ttfb_ms = (self._ttfb_time - self._start_time) * MS_IN_SECOND
    return timing

WaterfallVisualizer

WaterfallVisualizer(
    console: Console, max_bar_width: int = 80
)

Bases: Visualizer

Creates waterfall diagrams showing request phase timelines.

Renders each request phase (DNS, connect, TLS, wait, transfer) as a horizontally offset bar in the terminal using Rich, so the relative cost of each phase is visible at a glance. Implements the :class:~httptap.interfaces.Visualizer protocol.

属性:

  • console

    Rich console used for output.

  • max_bar_width

    Maximum width, in characters, of the timeline bars.

参数:

  • console (Console) –

    Rich console instance used to print the timeline.

  • max_bar_width (int, 默认: 80 ) –

    Maximum width, in characters, of the timeline bars.

方法:

  • render

    Render a waterfall timeline for the provided step if data is valid.

源代码位于: httptap/visualizer.py
def __init__(self, console: Console, max_bar_width: int = 80) -> None:
    """Configure the visualizer with a console and maximum bar width.

    Args:
        console: Rich console instance used to print the timeline.
        max_bar_width: Maximum width, in characters, of the timeline bars.
    """
    self.console = console
    self.max_bar_width = max_bar_width

render

render(step: StepMetrics) -> None

Render a waterfall timeline for the provided step if data is valid.

Steps that errored or have a non-positive total time are skipped and produce no output.

参数:

  • step (StepMetrics) –

    Step metrics containing timing, network, and response data.

源代码位于: httptap/visualizer.py
def render(self, step: StepMetrics) -> None:
    """Render a waterfall timeline for the provided step if data is valid.

    Steps that errored or have a non-positive total time are skipped and
    produce no output.

    Args:
        step: Step metrics containing timing, network, and response data.
    """
    if step.has_error or step.timing.total_ms <= 0:
        return

    phases = self._get_phases(step)
    durations = [duration for _, duration, _ in phases]
    bar_widths = self._compute_phase_widths(durations)
    used_width = sum(bar_widths) or 1
    scale = step.timing.total_ms / used_width

    self.console.print("\n  [bold]Request Timeline:[/bold]")

    current_position_chars = 0
    for (label, duration, color), bar_width in zip(phases, bar_widths, strict=True):
        current_position_chars = self._render_phase(
            label,
            duration,
            color,
            current_position_chars,
            bar_width,
        )

    self._render_total(step.timing.total_ms, scale)

JSONExporter

JSONExporter(console: Console)

Bases: Exporter

Exports HTTP analysis data to JSON format.

Handles serialization of step metrics and summary information to structured JSON files.

属性:

  • console

    Rich console for user feedback.

参数:

  • console (Console) –

    Rich console instance.

方法:

  • export

    Export analysis data to JSON file.

源代码位于: httptap/exporter.py
def __init__(self, console: Console) -> None:
    """Initialize JSON exporter.

    Args:
        console: Rich console instance.

    """
    self.console = console

export

export(
    steps: Sequence[StepMetrics],
    initial_url: str,
    output_path: str,
    *,
    slo_result: SLOResult | None = None,
) -> None

Export analysis data to JSON file.

Creates a structured JSON file with all step metrics, timing information, and summary data.

参数:

  • steps (Sequence[StepMetrics]) –

    Sequence of step metrics to export.

  • initial_url (str) –

    Initial URL that was analyzed.

  • output_path (str) –

    Path to output JSON file.

  • slo_result (SLOResult | None, 默认: None ) –

    Optional SLO evaluation result to embed under summary.slo.

引发:

  • IOError

    If file cannot be written.

源代码位于: httptap/exporter.py
def export(
    self,
    steps: Sequence[StepMetrics],
    initial_url: str,
    output_path: str,
    *,
    slo_result: SLOResult | None = None,
) -> None:
    """Export analysis data to JSON file.

    Creates a structured JSON file with all step metrics,
    timing information, and summary data.

    Args:
        steps: Sequence of step metrics to export.
        initial_url: Initial URL that was analyzed.
        output_path: Path to output JSON file.
        slo_result: Optional SLO evaluation result to embed under
            ``summary.slo``.

    Raises:
        IOError: If file cannot be written.

    """
    data = self._build_export_data(steps, initial_url, slo_result=slo_result)
    self._write_json_file(data, output_path)
    self._print_success(output_path)

HTTPClientRequestExecutor

RequestExecutor that delegates to the built-in HTTP client.

This is the default executor used by :class:~httptap.analyzer.HTTPTapAnalyzer. It forwards each request to :func:httptap.http_client.make_request, which instruments DNS, connection, TLS, and transfer timing.

方法:

  • execute

    Perform an HTTP request using the default client.

execute

execute(options: RequestOptions) -> RequestOutcome

Perform an HTTP request using the default client.

参数:

返回:

  • RequestOutcome

    A RequestOutcome bundling the timing, network, and response data.

引发:

  • HTTPClientError

    If the underlying HTTP request fails.

源代码位于: httptap/request_executor.py
def execute(self, options: RequestOptions) -> RequestOutcome:
    """Perform an HTTP request using the default client.

    Args:
        options: Fully populated request parameters.

    Returns:
        A RequestOutcome bundling the timing, network, and response data.

    Raises:
        httptap.http_client.HTTPClientError: If the underlying HTTP request
            fails.
    """
    timing, network, response = make_request(
        options.url,
        options.timeout,
        method=options.method,
        content=options.content,
        http2=options.http2,
        verify_ssl=options.verify_ssl,
        ca_bundle_path=options.ca_bundle_path,
        proxy=options.proxy,
        noproxy=options.noproxy,
        dns_resolver=options.dns_resolver,
        tls_inspector=options.tls_inspector,
        timing_collector=options.timing_collector,
        force_new_connection=options.force_new_connection,
        headers=options.headers,
    )
    return RequestOutcome(timing=timing, network=network, response=response)

SLO evaluation

The SLO helpers are exposed at the package root so programmatic callers can parse, evaluate, and serialize latency budgets exactly as the CLI does.

from httptap import HTTPTapAnalyzer, evaluate_slo, parse_slo_spec, select_step_for_evaluation

analyzer = HTTPTapAnalyzer(follow_redirects=True)
steps = analyzer.analyze_url("https://api.example.com/health")

thresholds = parse_slo_spec("total=500,ttfb=200")
target = select_step_for_evaluation(steps)
if target is not None:
    result = evaluate_slo(target, thresholds)
    if not result.passed:
        for violation in result.violations:
            print(f"{violation.key}: {violation.actual_ms:.1f}ms > {violation.threshold_ms:g}ms")

parse_slo_spec

parse_slo_spec(raw: str) -> dict[str, float]

Parse an --slo specification string.

The expected grammar is a comma-separated list of KEY=MS pairs. Whitespace around keys and values is tolerated. Keys are case-insensitive and normalised to lowercase.

参数:

  • raw (str) –

    String passed via --slo, e.g. "total=500,ttfb=200".

返回:

  • dict[str, float]

    Mapping of lowercase SLO key to threshold in milliseconds.

引发:

  • SLOSpecError

    If the string is empty, a pair is malformed, a key is unknown, a key is duplicated, or a value is not a positive, finite number.

示例:

>>> parse_slo_spec("total=500,ttfb=200")
{'total': 500.0, 'ttfb': 200.0}
>>> parse_slo_spec("Total=500")
{'total': 500.0}
源代码位于: httptap/slo.py
def parse_slo_spec(raw: str) -> dict[str, float]:
    """Parse an ``--slo`` specification string.

    The expected grammar is a comma-separated list of ``KEY=MS`` pairs.
    Whitespace around keys and values is tolerated. Keys are
    case-insensitive and normalised to lowercase.

    Args:
        raw: String passed via ``--slo``, e.g. ``"total=500,ttfb=200"``.

    Returns:
        Mapping of lowercase SLO key to threshold in milliseconds.

    Raises:
        SLOSpecError: If the string is empty, a pair is malformed, a
            key is unknown, a key is duplicated, or a value is not a
            positive, finite number.

    Examples:
        >>> parse_slo_spec("total=500,ttfb=200")
        {'total': 500.0, 'ttfb': 200.0}
        >>> parse_slo_spec("Total=500")
        {'total': 500.0}

    """
    if not raw or not raw.strip():
        msg = "SLO specification is empty (expected KEY=MS[,KEY=MS...])."
        raise SLOSpecError(msg)

    thresholds: dict[str, float] = {}

    for pair in raw.split(","):
        token = pair.strip()
        if not token:
            msg = f"Empty item in SLO specification '{raw}' (expected KEY=MS[,KEY=MS...])."
            raise SLOSpecError(msg)

        if token.count("=") != 1:
            msg = f"Invalid SLO item '{token}' (expected exactly one '=' between KEY and MS)."
            raise SLOSpecError(msg)

        key_raw, value_raw = token.split("=", 1)
        key = key_raw.strip().lower()
        value_str = value_raw.strip()

        if not key:
            msg = f"Invalid SLO item '{token}' (KEY must not be empty)."
            raise SLOSpecError(msg)

        if key not in SLO_KEYS:
            allowed = ", ".join(sorted(SLO_KEYS))
            msg = f"Unknown SLO key '{key}'. Valid keys: {allowed}."
            raise SLOSpecError(msg)

        if key in thresholds:
            msg = f"Duplicate SLO key '{key}' in specification '{raw}'."
            raise SLOSpecError(msg)

        try:
            value = float(value_str)
        except ValueError as exc:
            msg = f"Invalid SLO value for '{key}': '{value_str}' is not a number."
            raise SLOSpecError(msg) from exc

        if not math.isfinite(value) or value <= 0:
            msg = f"Invalid SLO value for '{key}': '{value_str}' must be a positive finite number of milliseconds."
            raise SLOSpecError(msg)

        thresholds[key] = value

    return thresholds

evaluate_slo

evaluate_slo(
    step: StepMetrics, thresholds: Mapping[str, float]
) -> SLOResult

Evaluate timings on a single step against SLO thresholds.

参数:

  • step (StepMetrics) –

    Step whose timing is compared to the thresholds.

  • thresholds (Mapping[str, float]) –

    Mapping of SLO key to threshold in milliseconds. Every key must be a member of :data:SLO_KEYS.

返回:

  • SLOResult

    class:SLOResult listing any violations in ascending

  • SLOResult

    alphabetical order of the user-supplied threshold keys. The

  • SLOResult

    order is stable so the JSON export is reproducible.

引发:

  • SLOSpecError

    If thresholds contains a key that is not a member of :data:SLO_KEYS. Programmatic callers are expected to validate input via :func:parse_slo_spec first; this check guards against accidental misuse.

源代码位于: httptap/slo.py
def evaluate_slo(
    step: StepMetrics,
    thresholds: Mapping[str, float],
) -> SLOResult:
    """Evaluate timings on a single step against SLO thresholds.

    Args:
        step: Step whose ``timing`` is compared to the thresholds.
        thresholds: Mapping of SLO key to threshold in milliseconds.
            Every key must be a member of :data:`SLO_KEYS`.

    Returns:
        :class:`SLOResult` listing any violations in ascending
        alphabetical order of the user-supplied threshold keys. The
        order is stable so the JSON export is reproducible.

    Raises:
        SLOSpecError: If ``thresholds`` contains a key that is not a
            member of :data:`SLO_KEYS`. Programmatic callers are
            expected to validate input via :func:`parse_slo_spec`
            first; this check guards against accidental misuse.

    """
    unknown = set(thresholds) - SLO_KEYS
    if unknown:
        allowed = ", ".join(sorted(SLO_KEYS))
        bad = ", ".join(sorted(unknown))
        msg = f"Unknown SLO key(s): {bad}. Valid keys: {allowed}."
        raise SLOSpecError(msg)

    # Keep this mapping in lockstep with SLO_KEYS (the frozenset above).
    timing_map: dict[str, float] = {
        "dns": step.timing.dns_ms,
        "connect": step.timing.connect_ms,
        "tls": step.timing.tls_ms,
        "ttfb": step.timing.ttfb_ms,
        "wait": step.timing.wait_ms,
        "xfer": step.timing.xfer_ms,
        "total": step.timing.total_ms,
    }

    violations = tuple(
        SLOViolation(key=key, threshold_ms=thresholds[key], actual_ms=timing_map[key])
        for key in sorted(thresholds)
        if timing_map[key] > thresholds[key]
    )

    return SLOResult(thresholds_ms=dict(thresholds), violations=violations)

select_step_for_evaluation

select_step_for_evaluation(
    steps: Sequence[StepMetrics],
) -> StepMetrics | None

Pick the step whose timing should be checked against SLO.

By convention, SLOs apply to the final successful step of a redirect chain (the one that actually served the user's request). If every step errored out, returns None — the caller is expected to treat that as a network failure rather than an SLO violation.

参数:

  • steps (Sequence[StepMetrics]) –

    Steps returned by HTTPTapAnalyzer.analyze_url.

返回:

  • StepMetrics | None

    Final successful step, or None if there is no such step.

源代码位于: httptap/slo.py
def select_step_for_evaluation(steps: Sequence[StepMetrics]) -> StepMetrics | None:
    """Pick the step whose timing should be checked against SLO.

    By convention, SLOs apply to the *final* successful step of a
    redirect chain (the one that actually served the user's request).
    If every step errored out, returns ``None`` — the caller is
    expected to treat that as a network failure rather than an SLO
    violation.

    Args:
        steps: Steps returned by ``HTTPTapAnalyzer.analyze_url``.

    Returns:
        Final successful step, or ``None`` if there is no such step.

    """
    for step in reversed(steps):
        if not step.has_error:
            return step
    return None

SLOResult dataclass

SLOResult(
    thresholds_ms: dict[str, float] = dict(),
    violations: tuple[SLOViolation, ...] = (),
)

Result of evaluating timings against a set of SLO thresholds.

Produced by :func:evaluate_slo, which guarantees the contract used by the rest of the codebase:

  • thresholds_ms is a fresh dict snapshot of the input mapping — safe to mutate by the caller without affecting the result object's serialization.
  • violations is a tuple sorted alphabetically by :attr:SLOViolation.key, making :meth:to_dict output byte-stable across evaluations with the same input.

The dataclass is frozen=True so attribute reassignment is blocked; this is shallow immutability (as with any Python frozen dataclass) — external callers who hand-construct SLOResult with mutable containers should not mutate them after handover.

:class:SLOResult is not hashable (thresholds_ms is a dict) and therefore cannot be used as a dict key or added to a set; equality comparison via == works and is structural.

属性:

  • thresholds_ms (dict[str, float]) –

    User-supplied budgets keyed by SLO key.

  • violations (tuple[SLOViolation, ...]) –

    Violations sorted alphabetically by key. When empty, the evaluation passed.

方法:

  • to_dict

    Serialize to a plain dict for JSON export.

passed property

passed: bool

Return True when every threshold was met or undercut.

to_dict

to_dict() -> dict[str, Any]

Serialize to a plain dict for JSON export.

Threshold keys are emitted in alphabetical order so that two evaluations with the same user input always produce byte-identical JSON (useful for diffing and cache keys).

返回:

  • dict[str, Any]

    Mapping with keys pass, thresholds_ms, violations.

源代码位于: httptap/slo.py
def to_dict(self) -> dict[str, Any]:
    """Serialize to a plain ``dict`` for JSON export.

    Threshold keys are emitted in alphabetical order so that two
    evaluations with the same user input always produce
    byte-identical JSON (useful for diffing and cache keys).

    Returns:
        Mapping with keys ``pass``, ``thresholds_ms``, ``violations``.

    """
    return {
        "pass": self.passed,
        "thresholds_ms": {key: self.thresholds_ms[key] for key in sorted(self.thresholds_ms)},
        "violations": [v.to_dict() for v in self.violations],
    }

SLOViolation dataclass

SLOViolation(
    key: str, threshold_ms: float, actual_ms: float
)

A single threshold violation.

属性:

  • key (str) –

    Timing phase key (e.g., "total", "ttfb").

  • threshold_ms (float) –

    Budget supplied by the user, in milliseconds.

  • actual_ms (float) –

    Measured timing value, in milliseconds.

方法:

  • to_dict

    Serialize to a plain dict for JSON export.

delta_ms property

delta_ms: float

Overrun over the budget, in milliseconds.

By construction :func:evaluate_slo only emits violations where actual_ms > threshold_ms, so this value is always strictly positive for objects produced by this module. Callers that instantiate SLOViolation directly should preserve this invariant to keep downstream output consistent.

to_dict

to_dict() -> dict[str, float | str]

Serialize to a plain dict for JSON export.

返回:

  • dict[str, float | str]

    Mapping with keys key, threshold_ms, actual_ms,

  • dict[str, float | str]

    delta_ms.

源代码位于: httptap/slo.py
def to_dict(self) -> dict[str, float | str]:
    """Serialize to a plain ``dict`` for JSON export.

    Returns:
        Mapping with keys ``key``, ``threshold_ms``, ``actual_ms``,
        ``delta_ms``.

    """
    return {
        "key": self.key,
        "threshold_ms": self.threshold_ms,
        "actual_ms": self.actual_ms,
        "delta_ms": self.delta_ms,
    }

SLOSpecError

Bases: ValueError

Raised when an --slo specification cannot be parsed.

SLO_KEYS module-attribute

SLO_KEYS: frozenset[str] = frozenset(
    {
        "dns",
        "connect",
        "tls",
        "ttfb",
        "wait",
        "xfer",
        "total",
    }
)

Error handling

httptap returns errors as part of StepMetrics rather than raising during analysis.

from httptap import HTTPTapAnalyzer

analyzer = HTTPTapAnalyzer()
steps = analyzer.analyze_url("https://invalid-domain.example")

step = steps[0]
if step.has_error:
    print(f"Error: {step.error}")
else:
    print(f"Status: {step.response.status}")

What's Next?