Skip to content

Hooks

Aletheca supports request/response hooks through bibliofabric's hook system. Hooks are callables invoked at specific points in the request lifecycle, allowing you to add custom logging, metrics, header injection, parameter modification, or response inspection without subclassing the client.

Hook Types

Pre-Request Hooks

Called before each HTTP request is sent. Hooks can modify query parameters and headers in place.

from collections.abc import Callable
from typing import Any

PreRequestHook = Callable[[str, str, dict[str, Any] | None, httpx.Headers], None]

Parameters:

Parameter Type Mutable Description
method str No HTTP method ("GET", "POST", etc.)
url str No Full request URL
params dict[str, Any] \| None Yes Query parameters — modify in place
headers httpx.Headers Yes Request headers — modify in place

Post-Request Hooks

Called after a response is received and parsed. Hooks receive the raw response, the parsed model (if any), and the number of attempts.

PostRequestHook = Callable[[httpx.Response, Any, int], None]

Parameters:

Parameter Type Description
response httpx.Response The raw HTTP response
parsed_model Any Parsed Pydantic model, or None if parsing failed
attempts int Number of request attempts made (always 1 for single requests)

Registering Hooks

Hooks are configured through AlethecaSettings:

from aletheca import AlethecaSession
from aletheca.config import AlethecaSettings

def log_request(method, url, params, headers):
    print(f"→ {method} {url} params={params}")

def log_response(response, parsed_model, attempts):
    print(f"← {response.status_code} (attempts: {attempts})")

settings = AlethecaSettings(
    pre_request_hooks=[log_request],
    post_request_hooks=[log_response],
)

async with AlethecaSession(settings=settings) as session:
    work = await session.works.get("W2741809807")

Examples

Custom Request Logger

import time

class RequestTimer:
    """Measure and log request durations via pre/post hooks."""

    def __init__(self):
        self._start_times: dict[str, float] = {}

    def before(self, method, url, params, headers):
        self._start_times[url] = time.monotonic()

    def after(self, response, parsed_model, attempts):
        url = str(response.url)
        elapsed = time.monotonic() - self._start_times.pop(url, 0)
        print(f"{response.status_code} {url} — {elapsed:.3f}s ({attempts} attempts)")

timer = RequestTimer()

settings = AlethecaSettings(
    pre_request_hooks=[timer.before],
    post_request_hooks=[timer.after],
)

async with AlethecaSession(settings=settings) as session:
    work = await session.works.get("W2741809807")

Add Custom Headers

def add_correlation_id(method, url, params, headers):
    import uuid
    headers["X-Correlation-ID"] = str(uuid.uuid4())

settings = AlethecaSettings(
    pre_request_hooks=[add_correlation_id],
)

Response Metrics

class MetricsCollector:
    """Collect response status code counts."""

    def __init__(self):
        self.status_codes: dict[int, int] = {}

    def record(self, response, parsed_model, attempts):
        code = response.status_code
        self.status_codes[code] = self.status_codes.get(code, 0) + 1

metrics = MetricsCollector()
settings = AlethecaSettings(post_request_hooks=[metrics.record])

async with AlethecaSession(settings=settings) as session:
    # ... make requests ...
    pass

print(metrics.status_codes)  # e.g., {200: 150, 429: 2}

Error Handling in Hooks

Hook errors are caught and logged — they do not abort the request. If a hook raises an exception, the error is logged at ERROR level and processing continues with the next hook or the request itself.

def flaky_hook(method, url, params, headers):
    raise RuntimeError("This hook fails but doesn't break the request")

# This is safe — the request still completes
settings = AlethecaSettings(pre_request_hooks=[flaky_hook])