# timeout-sampler

> Poll any function until it succeeds or times out, with fine-grained exception handling

---

Source: quickstart.md

## Prerequisites

- Python 3.10 or later
- `pip` (or any Python package manager)

## Install

```bash
pip install timeout-sampler
```

## Quick Example

```python
from timeout_sampler import TimeoutSampler

for sample in TimeoutSampler(wait_timeout=10, sleep=2, func=lambda: True):
    if sample:
        print("Got a truthy value, done!")
        break
```

That's it — `TimeoutSampler` calls your function every `sleep` seconds. Each iteration yields the return value so you can inspect it. If `wait_timeout` seconds elapse without a `break`, a `TimeoutExpiredError` is raised.

## Step-by-Step Walkthrough

### 1. Import the essentials

```python
from timeout_sampler import TimeoutSampler, TimeoutExpiredError
```

### 2. Define the function you want to poll

Any callable works — a regular function, a lambda, or a method:

```python
import random

def check_service():
    """Simulate a service that becomes ready after a few attempts."""
    return random.random() > 0.7
```

### 3. Create the sampler and iterate

```python
sampler = TimeoutSampler(
    wait_timeout=30,   # total seconds to wait
    sleep=5,           # seconds between retries
    func=check_service,
)

for sample in sampler:
    if sample:
        print("Service is ready!")
        break
```

If `check_service()` never returns a truthy value within 30 seconds, a `TimeoutExpiredError` is raised automatically after the loop ends.

### 4. Handle the timeout

Wrap the loop in a `try`/`except` when you need to react to a timeout:

```python
try:
    for sample in TimeoutSampler(wait_timeout=10, sleep=2, func=check_service):
        if sample:
            break
except TimeoutExpiredError as e:
    print(f"Timed out: {e}")
```

`TimeoutExpiredError` exposes two useful attributes:

| Attribute      | Type               | Description                                      |
|----------------|--------------------|--------------------------------------------------|
| `last_exp`     | `Exception | None` | The last exception raised inside `func`, if any  |
| `elapsed_time` | `float | None`     | Seconds elapsed before the error was raised       |

### 5. Pass arguments to your function

Use keyword arguments directly on the `TimeoutSampler` constructor — they are forwarded to `func`:

```python
def is_ready(host, port):
    # ... check connection ...
    return True

for sample in TimeoutSampler(
    wait_timeout=30,
    sleep=5,
    func=is_ready,
    host="localhost",
    port=8080,
):
    if sample:
        break
```

## Use the `@retry` Decorator

For the common pattern of "poll until truthy, then return the value," the `@retry` decorator eliminates the `for` loop entirely:

```python
from timeout_sampler import retry

@retry(wait_timeout=10, sleep=2)
def get_value():
    # return a truthy value when ready
    return True

result = get_value()  # blocks until truthy or TimeoutExpiredError
```

If `get_value()` keeps returning a falsy value for 10 seconds, `TimeoutExpiredError` is raised. See [Retrying Functions with the @retry Decorator](using-the-retry-decorator.html) for full decorator options.

## Advanced Usage

### Handling exceptions during polling

By default, `TimeoutSampler` catches **all** exceptions raised inside `func` and keeps retrying. You can control this with `exceptions_dict`:

```python
for sample in TimeoutSampler(
    wait_timeout=20,
    sleep=3,
    func=check_service,
    exceptions_dict={ConnectionError: []},
):
    if sample:
        break
```

- `{ConnectionError: []}` — ignore all `ConnectionError` instances (and subclasses) and keep polling.
- `{ConnectionError: ["refused"]}` — only ignore a `ConnectionError` whose message contains `"refused"`; any other `ConnectionError` re-raises immediately.
- `{HttpError: [lambda exc: exc.status >= 500]}` — use a **callable filter** to retry only when the exception's `status` attribute is 500 or above; 4xx errors re-raise immediately.
- `{HttpError: ["connection refused", lambda exc: exc.status >= 500]}` — combine string and callable filters in the same list; the exception is ignored if *any* filter matches.
- `{}` — do **not** ignore any exceptions; every exception re-raises immediately.

> **Warning:** Passing an empty dict `{}` means *no* exceptions are caught. If you want to catch all exceptions (the default), omit `exceptions_dict` entirely or pass `{Exception: []}`.

See [Filtering and Handling Exceptions](handling-exceptions.html) for the full inheritance-aware matching rules.

### Controlling log output

`TimeoutSampler` logs elapsed time and function call details by default. Toggle these with three boolean flags:

| Parameter         | Default | Effect                                                 |
|-------------------|---------|--------------------------------------------------------|
| `print_log`       | `True`  | Log elapsed time on each iteration                     |
| `print_func_log`  | `True`  | Include function name and module in log messages       |
| `print_func_args` | `True`  | Include `args`/`kwargs` in the function log            |

```python
for sample in TimeoutSampler(
    wait_timeout=10,
    sleep=2,
    func=check_service,
    print_log=False,        # silence all log output
):
    if sample:
        break
```

Sensitive keyword arguments such as `Authorization`, `token`, `password`, and `api_key` are **automatically redacted** from log output. To add your own keys, pass `sensitive_keys`:

```python
for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=1,
    func=call_api,
    sensitive_keys=frozenset({"x-custom-secret"}),
    headers={"Authorization": "Bearer token", "x-custom-secret": "value"}, # pragma: allowlist secret
):
    if sample:
        break
# Log output: Kwargs: {'headers': {'Authorization': '***', 'x-custom-secret': '***'}}
```

> **Tip:** The default sensitive keys are `authorization`, `token`, `access_token`, `password`, `secret`, `api_key`, and `apikey` (case-insensitive exact match). Custom keys from `sensitive_keys` are merged with the defaults.

See [Controlling Log Output](controlling-logging.html) for details.

### Tracking elapsed time independently

The `TimeoutWatch` helper lets you build custom timing logic outside of `TimeoutSampler`:

```python
from timeout_sampler import TimeoutWatch

watch = TimeoutWatch(timeout=60.0)

while watch.remaining_time() > 0:
    # your custom logic here
    pass
```

See [Tracking Elapsed Time with TimeoutWatch](tracking-elapsed-time.html) for more.

## Troubleshooting

| Problem | Cause | Fix |
|---------|-------|-----|
| `TimeoutExpiredError` raised immediately | `wait_timeout` is too small or `0` | Increase `wait_timeout` to allow at least one poll cycle |
| Function arguments not reaching `func` | Passing args positionally | Pass arguments as **keyword arguments** on the `TimeoutSampler` constructor (e.g., `host="localhost"`) |
| All exceptions are silently swallowed | Default `exceptions_dict` is `{Exception: []}` | Pass a narrower `exceptions_dict` to only ignore expected exceptions |
| Loop never exits | `func` returns truthy but there is no `break` | Always `break` (or `return`) out of the `for` loop when you get the value you want |

> **Tip:** For copy-paste recipes covering common real-world scenarios, see [Common Polling Patterns](common-polling-patterns.html).

## Related Pages

- [Polling a Function with TimeoutSampler](polling-with-timeout-sampler.html)
- [Retrying Functions with the @retry Decorator](using-the-retry-decorator.html)
- [TimeoutSampler API](api-timeout-sampler.html)
- [Common Polling Patterns](common-polling-patterns.html)
- [Filtering and Handling Exceptions](handling-exceptions.html)

---

Source: polling-with-timeout-sampler.md

# Polling a Function with TimeoutSampler

Poll any callable at regular intervals, inspect each return value, and break out as soon as a success condition is met — all with a built-in timeout safety net.

## Prerequisites

- `timeout-sampler` installed in your project (see [Getting Started with timeout-sampler](quickstart.html))

## Quick Example

```python
from timeout_sampler import TimeoutSampler

for sample in TimeoutSampler(wait_timeout=30, sleep=5, func=my_check_function):
    if sample:
        break
```

This calls `my_check_function()` every 5 seconds for up to 30 seconds. As soon as it returns a truthy value, the loop breaks. If 30 seconds elapse without success, a `TimeoutExpiredError` is raised.

## Step-by-Step: Polling Until a Condition Is Met

### 1. Import `TimeoutSampler`

```python
from timeout_sampler import TimeoutSampler
```

### 2. Create the sampler and iterate

Pass your function, a total timeout, and a sleep interval between polls:

```python
sampler = TimeoutSampler(
    wait_timeout=60,   # total seconds to wait
    sleep=3,           # seconds between each call
    func=check_api_health,
)
```

### 3. Write the polling loop

Each iteration calls your function and yields the return value. Check the value and `break` (or `return`) when you're satisfied:

```python
for sample in sampler:
    if sample == "healthy":
        print("Service is ready!")
        break
```

> **Warning:** If you never `break` out of the loop and the timeout expires, `TimeoutExpiredError` is raised automatically. Always include a break condition.

### 4. Handle the timeout

Wrap the loop in a `try`/`except` if you want to handle a timeout gracefully:

```python
from timeout_sampler import TimeoutExpiredError, TimeoutSampler

try:
    for sample in TimeoutSampler(wait_timeout=10, sleep=2, func=get_status):
        if sample == "ready":
            break
except TimeoutExpiredError:
    print("Timed out waiting for readiness")
```

### Passing Arguments to Your Function

Supply positional arguments with `func_args` and keyword arguments directly as extra keyword arguments:

```python
def check_endpoint(url, timeout=5):
    # ... returns True/False
    ...

for sample in TimeoutSampler(
    wait_timeout=30,
    sleep=5,
    func=check_endpoint,
    func_args=("https://api.example.com/health",),
    timeout=5,
):
    if sample:
        break
```

- `func_args` — a tuple of positional arguments forwarded to `func`
- Any extra keyword arguments (like `timeout=5` above) are forwarded to `func` as `**kwargs`

### Evaluating Non-Boolean Return Values

The yielded `sample` is whatever your function returns. You can apply any condition, not just truthiness:

```python
for sample in TimeoutSampler(wait_timeout=60, sleep=2, func=get_pod_count):
    if sample is not None and sample >= 3:
        print(f"Reached {sample} pods")
        break
```

## Advanced Usage

### Ignoring Specific Exceptions

By default, `TimeoutSampler` uses `{Exception: []}` as its exception dictionary, which catches and ignores all exceptions raised by your function during polling. To be more selective, pass `exceptions_dict`:

```python
for sample in TimeoutSampler(
    wait_timeout=30,
    sleep=2,
    func=fetch_data,
    exceptions_dict={ConnectionError: [], TimeoutError: []},
):
    if sample:
        break
```

- An empty list `[]` means "ignore this exception regardless of its message."
- A list of strings matches against the exception message text — only matching messages are ignored.
- A list can also contain **callables** that receive the exception instance and return a truthy value to ignore (retry).

```python
exceptions_dict = {
    ConnectionError: ["connection refused", "reset by peer"],
    ValueError: [],  # ignore all ValueErrors
}
```

Use callable filters to retry based on exception attributes:

```python
# Only retry on HTTP 5xx errors; 4xx errors raise immediately.
for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=1,
    func=make_request,
    exceptions_dict={HttpError: [lambda exc: exc.status >= 500]},
):
    if sample:
        break
```

Callable and string filters can be combined in the same list:

```python
for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=1,
    func=make_request,
    exceptions_dict={HttpError: ["connection refused", lambda exc: exc.status >= 500]},
):
    if sample:
        break
```

Any exception **not** listed (or listed but with a non-matching message/callable) is immediately re-raised as a `TimeoutExpiredError`.

For full details on exception filtering, see [Filtering and Handling Exceptions](handling-exceptions.html) and [How Exception Matching Works](exception-matching-logic.html).

### Controlling Log Output

`TimeoutSampler` logs elapsed time and function details by default. Disable or customize logging with these flags:

| Parameter         | Type   | Default | Effect                                              |
|-------------------|--------|---------|-----------------------------------------------------|
| `print_log`       | `bool` | `True`  | Log elapsed time on each iteration                  |
| `print_func_log`  | `bool` | `True`  | Include function name and module in log messages     |
| `print_func_args` | `bool` | `True`  | Include function arguments in log (when `print_func_log` is `True`) |

```python
for sample in TimeoutSampler(
    wait_timeout=30,
    sleep=5,
    func=my_func,
    print_log=False,        # suppress all elapsed-time logging
    print_func_log=False,   # suppress function call details
):
    if sample:
        break
```

#### Redacting Sensitive Data from Logs

Sensitive keyword argument values (such as `Authorization`, `token`, `password`, `secret`, `api_key`, and `apikey`) are automatically redacted from log output:

```python
for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=1,
    func=make_request,
    headers={"Authorization": "Bearer my-secret-token"},
):
    if sample:
        break
# Log output will show: Kwargs: {'headers': {'Authorization': '***'}}
```

To add custom sensitive keys (merged with the defaults), use the `sensitive_keys` parameter:

```python
for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=1,
    func=call_api,
    sensitive_keys=frozenset({"x-custom-secret"}),
    headers={"Authorization": "Bearer token", "x-custom-secret": "value"}, # pragma: allowlist secret
):
    if sample:
        break
```

> **Note:** Key matching is case-insensitive and uses exact match — a key named `"token"` is redacted, but `"nextPageToken"` is not.

See [Controlling Log Output](controlling-logging.html) for more on logging behavior.

### Using the `@retry` Decorator Instead

If your polling loop always follows the simple pattern of "break when truthy," the `@retry` decorator provides a more compact alternative:

```python
from timeout_sampler import retry

@retry(wait_timeout=10, sleep=2)
def wait_for_ready():
    return check_readiness()
```

This is equivalent to writing the `TimeoutSampler` loop manually. See [Retrying Functions with the @retry Decorator](using-the-retry-decorator.html) for full decorator usage.

### Accessing Error Details After Timeout

When `TimeoutExpiredError` is raised, it carries diagnostic attributes:

```python
try:
    for sample in TimeoutSampler(wait_timeout=5, sleep=1, func=flaky_call):
        if sample:
            break
except TimeoutExpiredError as err:
    print(err)                # Human-readable message with elapsed time
    print(err.last_exp)       # The last exception raised by func (or None)
    print(err.elapsed_time)   # Seconds elapsed before timeout
```

See [TimeoutExpiredError Reference](api-exceptions.html) for the full exception API.

## Troubleshooting

**`TimeoutExpiredError` raised immediately**
Your `wait_timeout` is too short relative to how long `func` takes to execute. Ensure `wait_timeout` is large enough to allow at least one full call-and-sleep cycle.

**Exceptions from my function are silently swallowed**
The default `exceptions_dict` is `{Exception: []}`, which catches *everything*. Pass a narrower dictionary to let unexpected exceptions propagate. See [Filtering and Handling Exceptions](handling-exceptions.html).

**Loop never breaks even though my function returns data**
Make sure your break condition actually matches the return value. Yielded samples are the *exact* return value of your function — check for `None`, empty collections, or `0` if those are possible returns.

> **Tip:** For a full constructor reference including types and defaults, see [TimeoutSampler API](api-timeout-sampler.html).

## Related Pages

- [Getting Started with timeout-sampler](quickstart.html)
- [TimeoutSampler API](api-timeout-sampler.html)
- [Filtering and Handling Exceptions](handling-exceptions.html)
- [Retrying Functions with the @retry Decorator](using-the-retry-decorator.html)
- [Controlling Log Output](controlling-logging.html)

---

Source: using-the-retry-decorator.md

# Retrying Functions with the @retry Decorator

You want to automatically retry a function until it returns a truthy value—without writing a manual polling loop. The `@retry` decorator wraps your function so it keeps calling itself on an interval until it succeeds or a timeout expires.

## Prerequisites

- `timeout-sampler` installed in your environment. See [Getting Started with timeout-sampler](quickstart.html) for installation steps.
- Basic familiarity with Python decorators.

## Quick Example

```python
from timeout_sampler import retry

@retry(wait_timeout=30, sleep=5)
def check_service_health():
    response = requests.get("https://my-service/health")
    return response.status_code == 200

# Blocks until the function returns True or 30 seconds elapse
check_service_health()
```

That's it — the decorator handles all the polling. If `check_service_health()` doesn't return a truthy value within 30 seconds, a `TimeoutExpiredError` is raised.

## How It Works

1. **Decorate** your function with `@retry(wait_timeout=..., sleep=...)`.
2. **Call** the function normally — arguments are passed through.
3. The decorator calls your function every `sleep` seconds.
4. As soon as the function returns a **truthy** value, that value is returned to the caller.
5. If the timeout expires without a truthy return, `TimeoutExpiredError` is raised.

## Parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `wait_timeout` | `int` | *(required)* | Maximum seconds to keep retrying |
| `sleep` | `int` | *(required)* | Seconds to wait between each attempt |
| `exceptions_dict` | `ExceptionsDict \| None` | `None` | Exceptions to tolerate during polling. Keys are exception classes; values are lists of string or callable filters. |
| `print_log` | `bool` | `True` | Log elapsed time to console |
| `print_func_log` | `bool` | `True` | Log function call details |
| `print_func_args` | `bool` | `True` | Include arguments in the function log |
| `sensitive_keys` | `frozenset[str] \| set[str] \| None` | `None` | Additional keys to redact from logged kwargs (case-insensitive). Merged with built-in defaults (`authorization`, `token`, `password`, `secret`, `api_key`, etc.). |

> **Tip:** For a complete parameter reference, see [@retry Decorator API](api-retry-decorator.html).

## Step-by-Step: Retrying Until a Condition Is Met

### 1. Define your function

Write a function that returns a truthy value on success and a falsy value (e.g., `False`, `None`, `0`, `""`) on failure.

```python
def is_database_ready():
    status = db.get_status()
    return status == "ready"
```

### 2. Apply the decorator

```python
from timeout_sampler import retry

@retry(wait_timeout=60, sleep=2)
def is_database_ready():
    status = db.get_status()
    return status == "ready"
```

### 3. Call the function

```python
is_database_ready()
print("Database is ready!")
```

### 4. Handle timeout

```python
from timeout_sampler import TimeoutExpiredError

try:
    is_database_ready()
except TimeoutExpiredError:
    print("Database did not become ready in time")
```

## Passing Arguments

The decorator passes through all positional and keyword arguments to your function:

```python
from timeout_sampler import retry

@retry(wait_timeout=30, sleep=3)
def wait_for_pod(namespace, name, status="Running"):
    pod = get_pod(namespace, name)
    return pod.status == status

# Arguments are forwarded to the decorated function
wait_for_pod("default", "my-pod", status="Running")
```

## Returning Values

When the function returns a truthy value, that value is returned to the caller—not just `True`:

```python
@retry(wait_timeout=20, sleep=2)
def fetch_result():
    result = get_async_result()
    return result  # Returns the actual result object when truthy

data = fetch_result()
print(data)  # The truthy value your function returned
```

> **Warning:** If your function returns a value that Python considers falsy (e.g., `0`, empty list `[]`, empty string `""`), the decorator treats it as a failed attempt and keeps retrying. Make sure success cases return a truthy value.

## Advanced Usage

### Tolerating Specific Exceptions

Use `exceptions_dict` to tell the decorator which exceptions should be ignored during polling instead of stopping execution. The keys are exception classes, and the values are lists of filters. Each filter can be:

- A **string** — matched as a substring against the exception message
- A **callable** — receives the exception instance and returns a truthy value to ignore (retry)
- An **empty list** — matches all instances of that exception type

```python
@retry(
    wait_timeout=30,
    sleep=5,
    exceptions_dict={ConnectionError: []},
)
def connect_to_service():
    return requests.get("https://my-service/api").ok
```

This keeps retrying even when `ConnectionError` is raised—useful for services that are still starting up.

You can also filter by exception message:

```python
@retry(
    wait_timeout=30,
    sleep=5,
    exceptions_dict={ConnectionError: ["Connection refused"]},
)
def connect_to_service():
    return requests.get("https://my-service/api").ok
```

Only `ConnectionError` exceptions containing `"Connection refused"` in their message text are tolerated. Other `ConnectionError` messages will stop polling.

You can also use callable filters to match on exception attributes:

```python
@retry(
    wait_timeout=60,
    sleep=1,
    exceptions_dict={HttpError: [lambda exc: exc.status >= 500]},
)
def make_api_call():
    return requests.get("https://my-service/api").json()
```

Only `HttpError` exceptions where `status >= 500` are tolerated — 4xx errors stop polling immediately.

String and callable filters can be combined in the same list. The exception is tolerated if **any** filter matches:

```python
@retry(
    wait_timeout=60,
    sleep=1,
    exceptions_dict={HttpError: ["connection refused", lambda exc: exc.status >= 500]},
)
def make_api_call():
    return requests.get("https://my-service/api").json()
```

> **Note:** For a detailed explanation of how exception matching and inheritance work, see [How Exception Matching Works](exception-matching-logic.html). For more `exceptions_dict` patterns, see [Filtering and Handling Exceptions](handling-exceptions.html).

### Controlling Log Output

By default, the decorator logs timing information and function details. You can turn these off individually:

```python
@retry(
    wait_timeout=10,
    sleep=1,
    print_log=False,       # Suppress all elapsed-time logs
)
def quiet_check():
    return some_condition()
```

```python
@retry(
    wait_timeout=10,
    sleep=1,
    print_func_log=False,  # Suppress function name in logs
    print_func_args=False,  # Suppress argument values in logs
)
def check_with_secrets(api_key):
    return validate(api_key)
```

### Redacting Sensitive Kwargs

Sensitive keyword argument values (such as `Authorization`, `token`, `password`, `secret`, `api_key`, and `apikey`) are automatically replaced with `"***"` in log output. You can add your own keys with `sensitive_keys`:

```python
@retry(
    wait_timeout=30,
    sleep=5,
    sensitive_keys=frozenset({"x-custom-secret"}),
)
def call_api(headers):
    return requests.get("https://my-service/api", headers=headers).ok

# The "x-custom-secret" value will appear as "***" in logs
call_api(headers={"Authorization": "Bearer token", "x-custom-secret": "value"}) # pragma: allowlist secret
```

> **Note:** Key matching is case-insensitive and uses exact match — a key named `"token"` is redacted, but `"nextPageToken"` is not.


> **Tip:** For more detail on logging options, see [Controlling Log Output](controlling-logging.html).

### When to Use @retry vs. TimeoutSampler

| | `@retry` | `TimeoutSampler` |
|---|---|---|
| **Best for** | Simple "retry until truthy" cases | Custom logic on each iteration |
| **Success condition** | Any truthy return value | You define it in the loop body |
| **Access to each result** | No — only the final truthy value | Yes — you inspect every yielded value |
| **Code style** | Decorator on function definition | Explicit `for` loop |

Use `@retry` when you just need a function to keep trying. Use `TimeoutSampler` when you need to examine intermediate results or apply complex success logic. See [Polling a Function with TimeoutSampler](polling-with-timeout-sampler.html) for the iterator approach.

## Troubleshooting

**`TimeoutExpiredError` is raised even though my function works**

Your function may be returning a falsy value on success. Check that it returns something truthy (e.g., `True`, a non-empty object) when the operation succeeds.

**Polling seems to stop too early when exceptions occur**

If your function raises an exception that isn't listed in `exceptions_dict`, polling will stop. Add the exception class to `exceptions_dict` to tolerate it. See [Filtering and Handling Exceptions](handling-exceptions.html) for details.

**Logs are too noisy**

Set `print_log=False` to suppress timing output, or set `print_func_args=False` to hide sensitive argument values. To redact specific keys rather than hiding all arguments, use `sensitive_keys`. See [Controlling Log Output](controlling-logging.html).

## Related Pages

- [@retry Decorator API](api-retry-decorator.html)
- [Polling a Function with TimeoutSampler](polling-with-timeout-sampler.html)
- [Filtering and Handling Exceptions](handling-exceptions.html)
- [Controlling Log Output](controlling-logging.html)
- [Getting Started with timeout-sampler](quickstart.html)

---

Source: handling-exceptions.md

# Filtering and Handling Exceptions

When polling a function that may intermittently fail, you need to control which exceptions are silently retried, which are matched by message text, and which immediately abort the loop. The `exceptions_dict` parameter gives you fine-grained control over all three behaviors.

## Prerequisites

- `timeout-sampler` installed in your project (see [Getting Started with timeout-sampler](quickstart.html))
- Basic familiarity with creating a polling loop (see [Polling a Function with TimeoutSampler](polling-with-timeout-sampler.html))

## Quick Example

```python
from timeout_sampler import TimeoutSampler

# Ignore all ConnectionError exceptions during polling
for sample in TimeoutSampler(
    wait_timeout=30,
    sleep=2,
    func=fetch_data,
    exceptions_dict={ConnectionError: []},
):
    if sample:
        break
```

An empty list `[]` means "ignore this exception regardless of its message text." If `fetch_data()` raises a `ConnectionError`, polling continues. Any other exception type immediately stops the loop.

## How `exceptions_dict` Works

The `exceptions_dict` parameter is a dictionary that maps exception classes to lists of filters. Each filter is either a substring to match against the exception message, or a callable that receives the exception and returns a truthy value to ignore it:

```python
from timeout_sampler import ExceptionsDict, ExceptionFilter

# Type aliases (importable from timeout_sampler):
# ExceptionFilter = str | Callable[[Exception], bool]
# ExceptionsDict = dict[type[Exception], list[ExceptionFilter]]

exceptions_dict: ExceptionsDict | None
```

| Value | Meaning |
|---|---|
| `{SomeError: []}` | Ignore **all** `SomeError` exceptions (any message) |
| `{SomeError: ["connection refused"]}` | Ignore `SomeError` only when the message **contains** `"connection refused"` |
| `{SomeError: ["timeout", "refused"]}` | Ignore `SomeError` when the message contains `"timeout"` **or** `"refused"` |
| `{SomeError: [lambda exc: exc.status >= 500]}` | Ignore `SomeError` only when the callable returns a truthy value |
| `{SomeError: ["refused", lambda exc: exc.retry]}` | Ignore when **any** filter matches (string **or** callable) |
| `{}` | Ignore **nothing** — any exception immediately stops polling |
| `None` (or omitted) | Defaults to `{Exception: []}` — ignore all exceptions |

> **Warning:** When you omit `exceptions_dict` entirely, **all** exceptions are silently ignored during polling. Always pass an explicit `exceptions_dict` in production to avoid swallowing unexpected errors.

## Step-by-Step: Common Use Cases

### 1. Ignore a Specific Exception Type

Pass the exception class with an empty list to ignore every instance of that exception:

```python
from timeout_sampler import TimeoutSampler

for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=5,
    func=check_service_health,
    exceptions_dict={ConnectionError: []},
):
    if sample == "healthy":
        break
```

### 2. Match by Message Text

Provide one or more substrings in the list. The exception is ignored only when any substring appears in the exception's text:

```python
from timeout_sampler import TimeoutSampler

for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=5,
    func=query_api,
    exceptions_dict={
        RuntimeError: ["temporarily unavailable", "rate limit"],
    },
):
    if sample:
        break
```

Here, a `RuntimeError("service temporarily unavailable")` is ignored (substring match), but a `RuntimeError("invalid credentials")` immediately stops polling.

> **Note:** Message matching uses a simple substring check (`msg in str(exception)`), not regex. The match is case-sensitive.

### 3. Match with a Callable Filter

When you need to inspect exception attributes (not just the message text), use a callable filter. The callable receives the exception instance and should return a truthy value to ignore (retry):

```python
from timeout_sampler import TimeoutSampler

# Only retry on HTTP 5xx errors; 4xx errors raise immediately
for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=1,
    func=make_request,
    exceptions_dict={HttpError: [lambda exc: exc.status >= 500]},
):
    if sample:
        break
```

Here, an `HttpError` with `status=502` is ignored, but an `HttpError` with `status=404` immediately stops polling.

> **Tip:** Callable filters must accept exactly one positional argument (the exception instance). If a callable raises an error at runtime (e.g., accessing a missing attribute), it is logged as a warning and treated as non-matching.

### 4. Combine String and Callable Filters

String and callable filters can be mixed in the same list. The exception is ignored if **any** filter matches:

```python
from timeout_sampler import TimeoutSampler

for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=1,
    func=make_request,
    exceptions_dict={
        HttpError: ["connection refused", lambda exc: exc.status >= 500],
    },
):
    if sample:
        break
```

This ignores `HttpError` when the message contains `"connection refused"` **or** when the status code is 500+.

### 5. Handle Multiple Exception Types

Add multiple entries to the dictionary, each with its own message filter:

```python
from timeout_sampler import TimeoutSampler

for sample in TimeoutSampler(
    wait_timeout=120,
    sleep=10,
    func=deploy_resource,
    exceptions_dict={
        ConnectionError: [],                      # ignore all connection errors
        TimeoutError: [],                         # ignore all timeout errors
        ValueError: ["not ready", "pending"],     # ignore only specific messages
    },
):
    if sample:
        break
```

### 6. Re-raise All Exceptions (No Filtering)

Pass an empty dictionary to ensure any exception immediately stops polling:

```python
from timeout_sampler import TimeoutSampler

for sample in TimeoutSampler(
    wait_timeout=30,
    sleep=2,
    func=critical_operation,
    exceptions_dict={},
):
    if sample:
        break
```

### 7. Use with the `@retry` Decorator

The `exceptions_dict` parameter works identically with the `@retry` decorator:

```python
from timeout_sampler import retry

@retry(
    wait_timeout=30,
    sleep=2,
    exceptions_dict={ConnectionError: []},
)
def fetch_data():
    # May raise ConnectionError intermittently
    return api_client.get("/data")
```

See [Retrying Functions with the @retry Decorator](using-the-retry-decorator.html) for full decorator usage.

## Advanced Usage

### Inheritance-Aware Matching

Exception matching respects Python's class hierarchy. When you add a parent exception class to `exceptions_dict`, **all child classes** are also matched:

```python
exceptions_dict = {ConnectionError: []}
```

| Raised Exception | Matched? | Reason |
|---|---|---|
| `ConnectionError` | ✅ Yes | Exact match |
| `ConnectionRefusedError` | ✅ Yes | Subclass of `ConnectionError` |
| `OSError` | ❌ No | Parent class, not a subclass |
| `ValueError` | ❌ No | Unrelated type |

This means you can filter broadly by specifying a base class, or narrowly by specifying a leaf class.

> **Tip:** Use `{Exception: []}` to ignore all exceptions (this is the default when `exceptions_dict` is omitted). Use a specific class like `{KeyError: []}` to only ignore that type and its subclasses.

### Three Outcome Categories

When your polled function raises an exception, exactly one of three things happens:

1. **Exact match or child class, filter matches** (substring found or callable returns truthy) → exception is ignored, polling continues
2. **Exact match or child class, no filter matches** → polling stops, `TimeoutExpiredError` is raised immediately
3. **Exception type not in `exceptions_dict`** → polling stops, `TimeoutExpiredError` is raised immediately

For a deeper look at the matching algorithm, see [How Exception Matching Works](exception-matching-logic.html).

### Accessing the Original Exception After Timeout

When polling ends — either by timeout or a non-matching exception — a `TimeoutExpiredError` is raised. The original exception is stored on its `last_exp` attribute:

```python
from timeout_sampler import TimeoutExpiredError, TimeoutSampler

try:
    for sample in TimeoutSampler(
        wait_timeout=10,
        sleep=2,
        func=flaky_function,
        exceptions_dict={ConnectionError: []},
    ):
        if sample:
            break
except TimeoutExpiredError as e:
    print(f"Last exception type: {type(e.last_exp)}")  # e.g. <class 'ConnectionError'>
    print(f"Last exception message: {e.last_exp}")
    print(f"Elapsed time: {e.elapsed_time}")
```

> **Note:** If the function never raised an exception (it just returned falsy values until timeout), `last_exp` is `None`.

See [TimeoutExpiredError Reference](api-exceptions.html) for all available attributes.

### Input Validation

The `exceptions_dict` is validated at `__init__` time. Invalid configurations raise `TypeError` immediately:

- **Empty strings** in filter lists are rejected — use an empty list `[]` to match all messages
- **Non-string, non-callable** filter items (e.g., `int`, `None`) are rejected
- **Classes** passed as filter items (e.g., `{ValueError: [TypeError]}`) are rejected — use a lambda instead
- **Keys** must be `Exception` subclasses
- **Values** must be lists

```python
# ❌ WRONG — empty string raises TypeError at init
exceptions_dict = {ValueError: [""]}

# ❌ WRONG — class passed as filter raises TypeError at init
exceptions_dict = {ValueError: [TypeError]}

# ✅ CORRECT — empty list means "match all messages"
exceptions_dict = {ValueError: []}

# ✅ CORRECT — callable filter
exceptions_dict = {ValueError: [lambda exc: "retry" in str(exc)]}
```

## Troubleshooting

| Problem | Cause | Solution |
|---|---|---|
| All exceptions are swallowed silently | `exceptions_dict` was omitted (defaults to `{Exception: []}`) | Pass an explicit `exceptions_dict` with only the types you want to ignore |
| Exception is not being ignored | The raised exception is a **parent** of the class in `exceptions_dict`, not a child | Add the parent class to `exceptions_dict`, or use a broader base class |
| Message filter doesn't match | Substring matching is case-sensitive | Verify the exact exception message text and case |
| Callable filter not working | The callable raises an error (e.g., accessing a missing attribute) | Check logs for "treating as non-matching" warnings; fix the callable |
| `TypeError` raised at init | Invalid `exceptions_dict` format (empty string, non-callable item, class as filter) | See [Input Validation](#input-validation) for valid formats |
| `TimeoutExpiredError` raised immediately despite exception being in dict | No filter in the list matches (neither substring nor callable) | Use `[]` to ignore all messages, or add the correct filter |

## Related Pages

- [How Exception Matching Works](exception-matching-logic.html)
- [Using Callable Exception Filters](callable-exception-filters.html)
- [TimeoutExpiredError Reference](api-exceptions.html)
- [Polling a Function with TimeoutSampler](polling-with-timeout-sampler.html)
- [Retrying Functions with the @retry Decorator](using-the-retry-decorator.html)

---

Source: controlling-logging.md

# Controlling Log Output

When debugging polling loops or running in production, you may want to control how much logging `timeout-sampler` produces. Three boolean parameters — `print_log`, `print_func_log`, and `print_func_args` — let you toggle elapsed-time messages, function-call details, and argument visibility independently.

## Prerequisites

- `timeout-sampler` installed in your project (see [Getting Started with timeout-sampler](quickstart.html))
- Basic familiarity with `TimeoutSampler` or the `@retry` decorator

## Quick Example

Suppress all log output by setting `print_log=False`:

```python
from timeout_sampler import TimeoutSampler

for sample in TimeoutSampler(
    wait_timeout=30,
    sleep=5,
    func=my_check,
    print_log=False,
):
    if sample:
        break
```

No log lines are emitted — no elapsed time, no function info, nothing.

## Understanding the Three Parameters

All three parameters default to `True`. Here's what each one controls:

| Parameter | Default | What it controls |
|---|---|---|
| `print_log` | `True` | Master switch — controls whether *any* log output is emitted |
| `print_func_log` | `True` | Adds the function name and module to the log line |
| `print_func_args` | `True` | Includes positional and keyword arguments in the function log |

> **Note:** `print_func_log` and `print_func_args` only take effect when `print_log` is `True`. Setting `print_log=False` silences everything regardless of the other two settings.

## Step-by-Step: Choosing a Logging Level

### 1. Full logging (default)

```python
sampler = TimeoutSampler(
    wait_timeout=60,
    sleep=5,
    func=check_service,
    func_args=("https://api.example.com",),
    retries=3,
)
```

Log output:

```
Waiting for 60 seconds [0:01:00], retry every 5 seconds. (Function: myapp.health.check_service Args: ('https://api.example.com',) Kwargs: {'retries': 3})
Elapsed time: 5.002 [0:00:05.002000]
```

### 2. Hide arguments only

When function arguments are too verbose:

```python
sampler = TimeoutSampler(
    wait_timeout=60,
    sleep=5,
    func=check_service,
    print_func_args=False,
    func_args=("https://api.example.com",),
    token="s3cret",
)
```

Log output:

```
Waiting for 60 seconds [0:01:00], retry every 5 seconds. (Function: myapp.health.check_service)
Elapsed time: 5.002 [0:00:05.002000]
```

The function name and module are still logged, but `Args` and `Kwargs` are omitted.

> **Tip:** If you want to keep argument logging but hide sensitive values like passwords or tokens, use the `sensitive_keys` parameter instead of disabling arguments entirely. See [Automatic Sensitive Key Redaction](#automatic-sensitive-key-redaction) below.

### 3. Hide function details entirely

When you only care about timing:

```python
sampler = TimeoutSampler(
    wait_timeout=60,
    sleep=5,
    func=check_service,
    print_func_log=False,
)
```

Log output:

```
Waiting for 60 seconds [0:01:00], retry every 5 seconds.
Elapsed time: 5.002 [0:00:05.002000]
```

> **Tip:** Setting `print_func_log=False` also suppresses argument output, so you don't need to set `print_func_args=False` separately.

### 4. Silence all logging

For production code, test suites, or inner loops where log noise is unwanted:

```python
sampler = TimeoutSampler(
    wait_timeout=60,
    sleep=5,
    func=check_service,
    print_log=False,
)
```

No log output is produced at all — neither the initial "Waiting for…" message nor the per-iteration elapsed-time lines.

## Using with the @retry Decorator

The same three parameters are available on the `@retry` decorator:

```python
from timeout_sampler import retry

@retry(wait_timeout=30, sleep=5, print_log=True, print_func_log=True, print_func_args=False)
def fetch_data(api_key):
    response = requests.get("https://api.example.com", headers={"Authorization": api_key})
    return response.ok
```

This logs the function name and elapsed time but omits the `api_key` argument from log output.

See [Retrying Functions with the @retry Decorator](using-the-retry-decorator.html) for full decorator usage.

## Parameter Combination Reference

| `print_log` | `print_func_log` | `print_func_args` | "Waiting for…" line | Function name in log | Args/Kwargs in log | Elapsed time lines |
|---|---|---|---|---|---|---|
| `True`  | `True`  | `True`  | ✅ | ✅ | ✅ | ✅ |
| `True`  | `True`  | `False` | ✅ | ✅ | ❌ | ✅ |
| `True`  | `False` | `True`  | ✅ | ❌ | ❌ | ✅ |
| `True`  | `False` | `False` | ✅ | ❌ | ❌ | ✅ |
| `False` | `True`  | `True`  | ❌ | ❌ | ❌ | ❌ |
| `False` | `True`  | `False` | ❌ | ❌ | ❌ | ❌ |
| `False` | `False` | `True`  | ❌ | ❌ | ❌ | ❌ |
| `False` | `False` | `False` | ❌ | ❌ | ❌ | ❌ |

> **Note:** When `print_func_log` is `False`, arguments are never shown — even if `print_func_args` is `True` — because the entire function info block is omitted.

## Advanced Usage

### Logging in Exception Scenarios

The `print_func_log` parameter also affects the error message inside `TimeoutExpiredError`. When a timeout expires:

- If `print_func_log=True`, the exception message includes the function name, module, and (if `print_func_args=True`) arguments.
- If `print_func_log=False`, the function info line in the exception message is empty.

```python
from timeout_sampler import TimeoutSampler, TimeoutExpiredError

try:
    for sample in TimeoutSampler(
        wait_timeout=5,
        sleep=1,
        func=my_check,
        print_func_log=True,
    ):
        if sample:
            break
except TimeoutExpiredError as e:
    # str(e) includes: "Function: mymodule.my_check"
    print(e)
```

See [TimeoutExpiredError Reference](api-exceptions.html) for details on exception attributes.

### Logging with Lambda and Partial Functions

`timeout-sampler` resolves function names through `functools.partial` wrappers and lambda expressions. When `print_func_log=True`, it follows partial chains to find the underlying function and displays lambda details including free variables and referenced names.

```python
from functools import partial

check = partial(requests.get, "https://example.com")

for sample in TimeoutSampler(
    wait_timeout=10,
    sleep=2,
    func=check,
    print_func_log=True,
    print_func_args=True,
):
    if sample.ok:
        break
```

The log will show the resolved underlying function name rather than `functools.partial`.

### Automatic Sensitive Key Redaction

When `print_func_args=True` (the default), `TimeoutSampler` automatically redacts values for common sensitive keyword argument keys before logging. Redacted values appear as `"***"` in the log output.

The default sensitive keys are: `authorization`, `token`, `access_token`, `password`, `secret`, `api_key`, and `apikey`. Matching is **case-insensitive** and **exact** — a key like `nextPageToken` is *not* redacted because it doesn't exactly match `token`.

```python
for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=1,
    func=make_request,
    headers={"Authorization": "Bearer my-secret-token"},
):
    if sample:
        break
# Log output: Kwargs: {'headers': {'Authorization': '***'}}
```

Redaction works recursively through nested dicts, lists, and tuples.

#### Adding custom sensitive keys

Pass `sensitive_keys` to add your own keys. Custom keys are **merged** with the defaults — they don't replace them:

```python
for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=1,
    func=call_api,
    sensitive_keys=frozenset({"x-custom-secret"}),
    headers={"Authorization": "Bearer token", "x-custom-secret": "value"}, # pragma: allowlist secret
):
    if sample:
        break
# Both "Authorization" and "x-custom-secret" values are redacted
```

The `sensitive_keys` parameter accepts `frozenset[str]` or `set[str]`. Passing an empty `frozenset()` still uses the default keys.

> **Warning:** `sensitive_keys` must contain only strings. Passing non-string elements raises `TypeError` at construction time.

The `sensitive_keys` parameter is also available on the `@retry` decorator:

```python
from timeout_sampler import retry

@retry(wait_timeout=30, sleep=5, sensitive_keys=frozenset({"x-api-secret"}))
def fetch_data(x_api_secret):
    return requests.get("https://api.example.com", headers={"x-api-secret": x_api_secret}).ok
```

### Selective Logging in Test Suites

When writing tests, suppress logging to keep test output clean:

```python
@retry(wait_timeout=5, sleep=1, print_log=False)
def wait_for_ready():
    return service.is_ready()
```

> **Tip:** The test suite for `timeout-sampler` itself uses `print_log=False` throughout to avoid noisy output during test runs.

## Troubleshooting

**Logs appear even though I set `print_func_log=False`**
The elapsed-time lines are controlled by `print_log`, not `print_func_log`. Set `print_log=False` to suppress all output, or leave `print_log=True` to keep only the timing information.

**Arguments still appear in `TimeoutExpiredError` messages**
The `print_func_args` parameter controls argument visibility in both the log output *and* the exception message. Verify that `print_func_args=False` is set on the `TimeoutSampler` or `@retry` call that raises the error.

**I want to customize the logger itself**
`timeout-sampler` uses `python-simple-logger` for its logging backend. The log parameters described on this page control *what* is logged, not *where* or *how*. To configure log levels, formats, or destinations, refer to `python-simple-logger` documentation.

**Sensitive values still appear in logs**
Redaction only applies to dict keys that exactly match (case-insensitive) a known sensitive key. If your secret is under a non-standard key name, add it via `sensitive_keys=frozenset({"my_key"})`. Redaction applies to kwargs, positional args containing dicts, and nested structures.

## Related Pages

- [TimeoutSampler API](api-timeout-sampler.html)
- [Retrying Functions with the @retry Decorator](using-the-retry-decorator.html)
- [Polling a Function with TimeoutSampler](polling-with-timeout-sampler.html)
- [@retry Decorator API](api-retry-decorator.html)
- [TimeoutExpiredError Reference](api-exceptions.html)

## Related Pages

- [Redacting Sensitive Data from Log Output](sensitive-key-redaction.html)
- [TimeoutSampler API](api-timeout-sampler.html)
- [Polling a Function with TimeoutSampler](polling-with-timeout-sampler.html)
- [Retrying Functions with the @retry Decorator](using-the-retry-decorator.html)
- [@retry Decorator API](api-retry-decorator.html)

---

Source: tracking-elapsed-time.md

# Tracking Elapsed Time with TimeoutWatch

Track how much time remains in a custom polling loop, orchestration workflow, or multi-step operation using `TimeoutWatch` — a lightweight countdown timer that starts when you create it.

## Prerequisites

- `timeout-sampler` installed in your project (see [Getting Started with timeout-sampler](quickstart.html))

## Quick Example

```python
from timeout_sampler import TimeoutWatch

watch = TimeoutWatch(timeout=30)

while watch.remaining_time() > 0:
    result = do_something()
    if result:
        break
```

`TimeoutWatch` records the current time when instantiated and returns how many seconds are left each time you call `remaining_time()`.

## Step-by-Step Usage

### 1. Create a TimeoutWatch

Pass the total number of seconds you want to track:

```python
from timeout_sampler import TimeoutWatch

watch = TimeoutWatch(timeout=60)
```

The countdown starts immediately — there is no separate `start()` call.

### 2. Check Remaining Time

Call `remaining_time()` to get the seconds left:

```python
seconds_left = watch.remaining_time()
print(f"{seconds_left:.1f} seconds remaining")
```

- Returns a `float` when time remains.
- Returns `0` once the timeout has elapsed (it never returns a negative value).

### 3. Use in a Loop

Build a polling loop that runs until the timeout expires:

```python
from timeout_sampler import TimeoutWatch

watch = TimeoutWatch(timeout=10)

while watch.remaining_time() > 0:
    status = check_service_health()
    if status == "ready":
        print("Service is up!")
        break
    time.sleep(1)
else:
    print("Timed out waiting for service.")
```

> **Tip:** The `while`/`else` pattern in Python lets you run the `else` block only when the loop condition becomes false — a clean way to handle timeouts without extra flags.

### 4. Calculate Elapsed Time

Since `TimeoutWatch` tracks remaining time, you can derive how much time has passed:

```python
watch = TimeoutWatch(timeout=30)

# ... some work ...

elapsed = watch.timeout - watch.remaining_time()
print(f"Elapsed: {elapsed:.2f}s")
```

This is the same technique that `TimeoutSampler` uses internally to report elapsed time in its logs.

## API Reference

### `TimeoutWatch(timeout)`

| Parameter | Type    | Description                          |
|-----------|---------|--------------------------------------|
| `timeout` | `float` | Total countdown duration in seconds |

Creates a new watch and records the start time immediately.

### `remaining_time()`

```python
def remaining_time(self) -> int | float
```

Returns the number of seconds left until the timeout expires. The return value is clamped to `0` — it will never be negative.

| Condition                      | Return value            |
|-------------------------------|-------------------------|
| Called before timeout expires | Positive `float`        |
| Called after timeout expires  | `0`                     |

## Advanced Usage

### Coordinating Multiple Steps Under One Budget

When you need several sequential operations to fit within a shared time budget, create one `TimeoutWatch` and pass its remaining time to each step:

```python
from timeout_sampler import TimeoutSampler, TimeoutWatch

overall = TimeoutWatch(timeout=120)

# Step 1: Wait for database
for sample in TimeoutSampler(
    wait_timeout=overall.remaining_time(),
    sleep=2,
    func=check_database,
):
    if sample:
        break

# Step 2: Wait for cache (uses whatever time is left)
for sample in TimeoutSampler(
    wait_timeout=overall.remaining_time(),
    sleep=2,
    func=check_cache,
):
    if sample:
        break
```

Each `TimeoutSampler` receives only the remaining portion of the overall budget, so the total wall-clock time never exceeds 120 seconds regardless of how long step 1 takes.

> **Note:** If `remaining_time()` returns `0` before a step begins, the `TimeoutSampler` will raise a `TimeoutExpiredError` immediately. See [TimeoutExpiredError Reference](api-exceptions.html) for details on that exception.

### Passing Fractional Timeouts

`TimeoutWatch` accepts `float` values, so sub-second precision works out of the box:

```python
watch = TimeoutWatch(timeout=0.5)
# Half-second budget
```

### Using TimeoutWatch Without TimeoutSampler

`TimeoutWatch` has no dependency on `TimeoutSampler` — use it anywhere you need a simple countdown:

```python
from timeout_sampler import TimeoutWatch

watch = TimeoutWatch(timeout=5)

items = get_work_items()
for item in items:
    if watch.remaining_time() == 0:
        print("Time budget exhausted, stopping early.")
        break
    process(item)
```

## Troubleshooting

| Problem | Cause | Fix |
|---------|-------|-----|
| `remaining_time()` returns `0` immediately | `timeout` was set to `0` or a negative value | Use a positive `timeout` value |
| Elapsed time calculation seems wrong | You created the `TimeoutWatch` too early (e.g., at module import time) | Create the instance right before the work begins |
| Loop never exits | Your loop body doesn't call `remaining_time()` on each iteration | Ensure the `while` condition re-evaluates `remaining_time()` every pass |

## Related Pages

- [Polling a Function with TimeoutSampler](polling-with-timeout-sampler.html) — the primary polling interface that uses `TimeoutWatch` under the hood
- [TimeoutWatch API](api-timeout-watch.html) — full constructor and method reference
- [TimeoutExpiredError Reference](api-exceptions.html) — the exception raised when time runs out

## Related Pages

- [TimeoutWatch API](api-timeout-watch.html)
- [Polling a Function with TimeoutSampler](polling-with-timeout-sampler.html)
- [TimeoutSampler API](api-timeout-sampler.html)
- [Common Polling Patterns](common-polling-patterns.html)
- [TimeoutExpiredError Reference](api-exceptions.html)

---

Source: common-polling-patterns.md

# Common Polling Patterns

Copy-paste recipes for the most frequent `timeout-sampler` use cases. Each recipe is self-contained and ready to drop into your project.

> **Note:** All recipes assume you have already installed the package. See [Getting Started with timeout-sampler](quickstart.html) for installation instructions.

## Wait for an API to Become Ready

Poll an HTTP endpoint until it returns a successful response.

```python
import requests
from timeout_sampler import TimeoutSampler

for sample in TimeoutSampler(
    wait_timeout=120,
    sleep=5,
    func=lambda: requests.get("http://localhost:8080/healthz").ok,
    exceptions_dict={requests.ConnectionError: [], requests.Timeout: []},
):
    if sample:
        break
```

The sampler calls the health-check endpoint every 5 seconds for up to 2 minutes. Connection errors and timeouts are silently retried thanks to `exceptions_dict`. The loop breaks as soon as the endpoint returns a 2xx response.

> **Tip:** For long startup waits, increase `wait_timeout` and keep `sleep` between 2–10 seconds to avoid hammering the service.

## Retry a Flaky Function with the @retry Decorator

Automatically re-run a function until it returns a truthy value.

```python
from timeout_sampler import retry

@retry(wait_timeout=30, sleep=2)
def fetch_cluster_status():
    import requests
    resp = requests.get("https://api.example.com/cluster/status")
    resp.raise_for_status()
    return resp.json()["state"] == "ready"

# Raises TimeoutExpiredError after 30s if the cluster never reaches "ready"
fetch_cluster_status()
```

The `@retry` decorator wraps the function in a `TimeoutSampler` loop and returns the first truthy result. Use it when you want polling behavior without writing the iteration yourself.

- The decorated function keeps its original signature — pass arguments as usual.
- Any unhandled exception is immediately re-raised unless you add `exceptions_dict`.

See [Retrying Functions with the @retry Decorator](using-the-retry-decorator.html) for full parameter details.

## Poll with a Partial Function

Use `functools.partial` to poll a function that requires arguments without using `func_args` or keyword arguments.

```python
from functools import partial
from timeout_sampler import TimeoutSampler

def check_pod_phase(namespace, pod_name):
    """Returns True when the pod is Running."""
    import subprocess, json
    result = subprocess.run(
        ["kubectl", "get", "pod", pod_name, "-n", namespace, "-o", "json"],
        capture_output=True, text=True,
    )
    pod = json.loads(result.stdout)
    return pod["status"]["phase"] == "Running"

poll_fn = partial(check_pod_phase, "default", "my-app-pod-7f4b9")

for sample in TimeoutSampler(wait_timeout=90, sleep=3, func=poll_fn):
    if sample:
        break
```

`TimeoutSampler` resolves `partial` objects automatically when building log output, so function names and modules are logged correctly even through the wrapper. This pattern keeps the sampler call clean when the polled function has many parameters.

## Pass Arguments via func_args and Keyword Arguments

Provide positional and keyword arguments directly to `TimeoutSampler` without wrapping in `partial`.

```python
from timeout_sampler import TimeoutSampler

def is_file_present(directory, filename, min_size_bytes=0):
    import os
    path = os.path.join(directory, filename)
    return os.path.isfile(path) and os.path.getsize(path) >= min_size_bytes

for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=2,
    func=is_file_present,
    func_args=("/tmp/exports", "report.csv"),
    min_size_bytes=1024,
):
    if sample:
        break
```

Positional arguments go into `func_args` as a tuple. Keyword arguments are passed directly as extra kwargs to the `TimeoutSampler` constructor, which forwards them to `func` on every call.

## Ignore All Instances of an Exception

Swallow every occurrence of a specific exception type during polling.

```python
from timeout_sampler import TimeoutSampler

def get_resource():
    import json, urllib.request
    resp = urllib.request.urlopen("http://localhost:9090/resource")
    return json.loads(resp.read())

for sample in TimeoutSampler(
    wait_timeout=30,
    sleep=2,
    func=get_resource,
    exceptions_dict={ConnectionError: [], TimeoutError: []},
):
    if sample:
        break
```

An empty list `[]` next to an exception class means *ignore all messages* for that exception. The sampler will keep retrying regardless of the exception's text content.

See [Filtering and Handling Exceptions](handling-exceptions.html) for a full explanation of `exceptions_dict`.

## Filter Exceptions by Message Text

Only ignore exceptions whose message matches specific substrings.

```python
from timeout_sampler import TimeoutSampler

def query_database():
    import sqlite3
    conn = sqlite3.connect("/var/data/app.db")
    cursor = conn.execute("SELECT count(*) FROM jobs WHERE status = 'done'")
    count = cursor.fetchone()[0]
    conn.close()
    if count == 0:
        raise RuntimeError("no completed jobs yet")
    return count

for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=5,
    func=query_database,
    exceptions_dict={RuntimeError: ["no completed jobs yet"]},
):
    if sample:
        print(f"Completed jobs: {sample}")
        break
```

The sampler checks whether the raised exception's string representation *contains* any of the listed substrings. If a `RuntimeError` is raised with a different message (e.g., `"database locked"`), it will **not** be caught — it will immediately raise a `TimeoutExpiredError`.

> **Warning:** Message matching uses substring `in` checks, not exact equality. The filter `"not found"` will also match `"resource not found in namespace"`.

## Filter Exceptions with Callables

Use callable filters to retry based on exception attributes instead of message text. The callable receives the exception instance and should return a truthy value to ignore (retry).

```python
from timeout_sampler import TimeoutSampler

class HttpError(Exception):
    def __init__(self, status: int, message: str) -> None:
        self.status = status
        super().__init__(message)

def make_request():
    # ... HTTP call that may raise HttpError ...
    return True

# Only retry on HTTP 5xx errors; 4xx errors raise immediately.
for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=1,
    func=make_request,
    exceptions_dict={HttpError: [lambda exc: exc.status >= 500]},
):
    if sample:
        break
```

Callable filters are useful when the exception carries structured data (status codes, error categories, retry hints) that cannot be matched with simple substring checks.

> **Tip:** Callable and string filters can be combined in the same list. The exception is ignored if **any** filter matches.

```python
for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=1,
    func=make_request,
    exceptions_dict={HttpError: ["connection refused", lambda exc: exc.status >= 500]},
):
    if sample:
        break
```

This retries on any `HttpError` whose message contains `"connection refused"` **or** whose `status` attribute is 500 or above.

## Combine Multiple Exception Filters

Handle several exception types, each with independent message filters.

```python
from timeout_sampler import TimeoutSampler

def provision_vm():
    """Calls a cloud API that may fail in multiple ways."""
    # ... cloud SDK call ...
    return {"id": "vm-abc123", "status": "running"}

for sample in TimeoutSampler(
    wait_timeout=300,
    sleep=10,
    func=provision_vm,
    exceptions_dict={
        ConnectionError: [],                          # retry on any connection issue
        TimeoutError: [],                             # retry on any timeout
        RuntimeError: ["quota exceeded", "retryable"],  # only retry these messages
        PermissionError: ["token expired"],            # only retry on expired tokens
    },
):
    if sample and sample.get("status") == "running":
        print(f"VM provisioned: {sample['id']}")
        break
```

Each exception class has its own message filter list. This lets you broadly retry transient network errors while being selective about application-level exceptions. Any exception type or message **not** listed will immediately surface as a `TimeoutExpiredError`.

See [How Exception Matching Works](exception-matching-logic.html) for the inheritance-aware matching algorithm.

## Leverage Exception Inheritance for Broad Matching

Catch a parent exception class to automatically cover all its subclasses.

```python
from timeout_sampler import TimeoutSampler

class ServiceError(Exception):
    pass

class TransientError(ServiceError):
    pass

class RateLimitError(ServiceError):
    pass

def call_external_service():
    # ... API call that may raise TransientError or RateLimitError ...
    return True

for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=3,
    func=call_external_service,
    exceptions_dict={ServiceError: []},
):
    if sample:
        break
```

Listing `ServiceError` in `exceptions_dict` catches both `TransientError` and `RateLimitError` because `TimeoutSampler` uses `isinstance()` to match exceptions. You don't need to enumerate every subclass individually.

> **Tip:** Use broad parent-class matching for exception hierarchies you control, and specific-class matching for third-party exceptions where you want precise control.

## Wait for a Return Value to Match a Condition

Poll until the function returns a specific value, not just a truthy one.

```python
from timeout_sampler import TimeoutSampler

def get_deployment_replicas():
    """Returns the current number of ready replicas."""
    import subprocess, json
    result = subprocess.run(
        ["kubectl", "get", "deployment", "web-api", "-o", "json"],
        capture_output=True, text=True,
    )
    deploy = json.loads(result.stdout)
    return deploy["status"].get("readyReplicas", 0)

desired_replicas = 3

for sample in TimeoutSampler(wait_timeout=120, sleep=5, func=get_deployment_replicas):
    if sample == desired_replicas:
        break
```

The `if` condition inside the loop is your match logic — you can check equality, membership, ranges, or any predicate. The sampler itself only yields; your code decides what constitutes success.

## Silence All Log Output

Disable logging for test suites or inner loops where verbosity is unwanted.

```python
from timeout_sampler import TimeoutSampler

for sample in TimeoutSampler(
    wait_timeout=10,
    sleep=1,
    func=lambda: True,
    print_log=False,
    print_func_log=False,
):
    if sample:
        break
```

Setting `print_log=False` suppresses elapsed-time messages, and `print_func_log=False` suppresses function call details. Use both together for completely silent polling.

See [Controlling Log Output](controlling-logging.html) for fine-grained logging options including `print_func_args`.

## Redact Sensitive Data from Logs

Sensitive kwargs such as `Authorization`, `token`, `password`, and `api_key` are automatically redacted from log output.

```python
from timeout_sampler import TimeoutSampler

for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=1,
    func=make_request,
    headers={"Authorization": "Bearer my-secret-token"},
):
    if sample:
        break
# Log output will show: Kwargs: {'headers': {'Authorization': '***'}}
```

The default sensitive keys are: `authorization`, `token`, `access_token`, `password`, `secret`, `api_key`, and `apikey`. Matching is case-insensitive and exact (e.g., `"token"` matches a key named `Token` but not `nextPageToken`).

To add custom sensitive keys, pass `sensitive_keys` — they are merged with the defaults:

```python
for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=1,
    func=call_api,
    sensitive_keys=frozenset({"x-custom-secret"}),
    headers={"Authorization": "Bearer token", "x-custom-secret": "value"}, # pragma: allowlist secret
):
    if sample:
        break
```

> **Note:** Redaction applies recursively to nested dicts, lists, and tuples in kwargs. The `sensitive_keys` parameter is also available on the `@retry` decorator.

## Track Remaining Time Across Multiple Polling Steps

Use `TimeoutWatch` to share a single time budget across sequential polling operations.

```python
from timeout_sampler import TimeoutSampler, TimeoutWatch

overall_timeout = TimeoutWatch(timeout=120)

# Step 1: Wait for database
for sample in TimeoutSampler(
    wait_timeout=overall_timeout.remaining_time(),
    sleep=3,
    func=lambda: __import__("os").path.exists("/tmp/db.ready"),
):
    if sample:
        break

# Step 2: Wait for cache (uses remaining time from same budget)
for sample in TimeoutSampler(
    wait_timeout=overall_timeout.remaining_time(),
    sleep=2,
    func=lambda: __import__("os").path.exists("/tmp/cache.ready"),
):
    if sample:
        break

print(f"Both ready with {overall_timeout.remaining_time():.1f}s to spare")
```

`TimeoutWatch.remaining_time()` returns the seconds left from the original timeout, automatically accounting for elapsed time. Pass it as `wait_timeout` to give each subsequent step only the remaining budget.

See [Tracking Elapsed Time with TimeoutWatch](tracking-elapsed-time.html) for the full `TimeoutWatch` API.

## Catch TimeoutExpiredError and Inspect the Last Exception

Access diagnostic information when polling fails.

```python
from timeout_sampler import TimeoutExpiredError, TimeoutSampler

def unstable_lookup():
    raise ConnectionError("connection refused on port 5432")

try:
    for sample in TimeoutSampler(
        wait_timeout=10,
        sleep=2,
        func=unstable_lookup,
        exceptions_dict={ConnectionError: []},
    ):
        if sample:
            break
except TimeoutExpiredError as e:
    print(f"Polling failed: {e}")
    print(f"Last exception type: {type(e.last_exp).__name__}")  # ConnectionError
    print(f"Last exception message: {e.last_exp}")              # connection refused on port 5432
    print(f"Total elapsed time: {e.elapsed_time}s")
```

`TimeoutExpiredError` exposes `last_exp` (the last exception raised by the polled function) and `elapsed_time` (total seconds spent polling). Use these for detailed error reporting or conditional recovery logic.

See [TimeoutExpiredError Reference](api-exceptions.html) for all available attributes.

## Related Pages

- [Polling a Function with TimeoutSampler](polling-with-timeout-sampler.html)
- [Filtering and Handling Exceptions](handling-exceptions.html)
- [Retrying Functions with the @retry Decorator](using-the-retry-decorator.html)
- [Tracking Elapsed Time with TimeoutWatch](tracking-elapsed-time.html)
- [Redacting Sensitive Data from Log Output](sensitive-key-redaction.html)

---

Source: api-timeout-sampler.md

# TimeoutSampler API

Complete reference for the `TimeoutSampler` class — constructor parameters, iteration protocol, exception handling, and return semantics.

```python
from timeout_sampler import TimeoutSampler
```

## Constructor

```python
TimeoutSampler(
    wait_timeout: float,
    sleep: int,
    func: Callable,
    exceptions_dict: ExceptionsDict | None = None,
    print_log: bool = True,
    print_func_log: bool = True,
    print_func_args: bool = True,
    sensitive_keys: frozenset[str] | set[str] | None = None,
    func_args: tuple[Any] | None = None,
    **func_kwargs: Any,
)
```

### Parameters

| Name | Type | Default | Description |
|---|---|---|---|
| `wait_timeout` | `float` | *(required)* | Maximum time in seconds to poll `func` before raising `TimeoutExpiredError`. |
| `sleep` | `int` | *(required)* | Time in seconds to sleep between successive calls to `func`. |
| `func` | `Callable` | *(required)* | The function to poll. Called as `func(*func_args, **func_kwargs)` on each iteration. |
| `exceptions_dict` | `ExceptionsDict \| None` | `None` | Map of exception types to filter lists. Filters can be strings (matched as substrings of `str(exception)`) or callables (invoked with the exception instance, returning truthy to ignore). When `None`, defaults to `{Exception: []}` (all exceptions ignored). See [How Exception Matching Works](exception-matching-logic.html). |
| `print_log` | `bool` | `True` | Log elapsed time on each iteration and print a summary line at the start. |
| `print_func_log` | `bool` | `True` | Include function module and name in the startup log message. |
| `print_func_args` | `bool` | `True` | Include `func_args` and `func_kwargs` in the log when `print_func_log` is `True`. |
| `sensitive_keys` | `frozenset[str] \| set[str] \| None` | `None` | Additional keys to redact from logged kwargs (case-insensitive exact match). Merged with the built-in default sensitive keys. See [Sensitive Key Redaction](#sensitive-key-redaction). |
| `func_args` | `tuple[Any] \| None` | `None` | Positional arguments forwarded to `func`. Stored as an empty tuple when `None`. |
| `**func_kwargs` | `Any` | — | Keyword arguments forwarded to `func`. |

> **Note:** When `exceptions_dict` is omitted (or `None`), it defaults to `{Exception: []}`, which silently ignores **all** exceptions raised inside `func` until the timeout expires. Pass an explicit empty dict `{}` to re-raise every exception immediately.


> **Note:** The `exceptions_dict` is validated at construction time. Keys must be `Exception` subclasses, values must be lists, and filter items must be non-empty strings or callables. Passing invalid types (e.g., a class instead of a callable, or an empty string) raises `TypeError` immediately.

### Example — Basic Construction

```python
from timeout_sampler import TimeoutSampler

def check_service():
    return {"status": "ready"}

sampler = TimeoutSampler(
    wait_timeout=30,
    sleep=5,
    func=check_service,
)
```

### Example — Passing Arguments to `func`

```python
import requests
from timeout_sampler import TimeoutSampler

sampler = TimeoutSampler(
    wait_timeout=60,
    sleep=2,
    func=requests.get,
    func_args=("https://api.example.com/health",),
    timeout=5,          # forwarded as requests.get(..., timeout=5)
)
```

---

## Iteration Protocol

`TimeoutSampler` implements `__iter__`. Use it in a `for` loop. Each iteration calls `func(*func_args, **func_kwargs)` and yields the return value.

```python
def __iter__(self) -> Any
```

**Yields:** The return value of `func` on each successful call.

**Raises:** [`TimeoutExpiredError`](api-exceptions.html) when the elapsed time exceeds `wait_timeout`.

### Iteration Lifecycle

1. A `TimeoutWatch` is created with `timeout=wait_timeout`.
2. While remaining time > 0:
   - `func(*func_args, **func_kwargs)` is called.
   - The return value is **yielded** to the caller.
   - After the caller processes the yielded value and continues the loop, the sampler sleeps for `sleep` seconds.
3. If the loop exhausts the timeout without the caller breaking out, `TimeoutExpiredError` is raised.

> **Warning:** `TimeoutSampler` does **not** evaluate the return value of `func`. The caller must inspect each yielded sample and `break` or `return` when a satisfactory value is found. Failing to break out of the loop will always result in `TimeoutExpiredError`.

### Example — Iterate Until Success

```python
from timeout_sampler import TimeoutSampler

def get_pod_status():
    # returns "Pending", "Running", etc.
    ...

for sample in TimeoutSampler(wait_timeout=120, sleep=5, func=get_pod_status):
    if sample == "Running":
        break
```

### Example — Iterate with Logging Disabled

```python
for sample in TimeoutSampler(
    wait_timeout=10,
    sleep=1,
    func=lambda: True,
    print_log=False,
):
    if sample:
        break
```

---

## Exception Handling During Iteration

When `func` raises an exception during iteration, `TimeoutSampler` checks it against `exceptions_dict` using `_should_ignore_exception` and `_is_exception_matched`.

For a detailed walkthrough of the matching algorithm, see [How Exception Matching Works](exception-matching-logic.html).

### `exceptions_dict` Format

```python
{
    ExceptionClass: ["message_substring", lambda exc: exc.attr > 0, ...],
    AnotherException: [],   # empty list = match all messages
}
```

Filter items can be **strings** (matched as substrings of `str(exception)`) or **callables** (invoked with the exception instance, returning a truthy value to ignore/retry). Both types can be combined in the same list.

| `exceptions_dict` value | Behavior |
|---|---|
| `None` (default) | Replaced internally with `{Exception: []}` — all exceptions are ignored until timeout. |
| `{}` (empty dict) | Every exception is immediately re-raised as `TimeoutExpiredError`. |
| `{ValueError: []}` | Any `ValueError` (or subclass) is ignored regardless of message text. |
| `{ValueError: ["connection"]}` | `ValueError` is ignored only if `"connection"` appears in `str(exp)`. |
| `{HttpError: [lambda exc: exc.status >= 500]}` | `HttpError` is ignored only if the callable returns truthy. |
| `{HttpError: ["connection refused", lambda exc: exc.status >= 500]}` | `HttpError` is ignored if **either** the string matches **or** the callable returns truthy. |
| `{KeyError: ["x"], IndexError: ["y"]}` | Multiple exception types, each with independent filters. |

> **Tip:** The match uses `isinstance()`, so a parent class in `exceptions_dict` will also catch child classes. See [How Exception Matching Works](exception-matching-logic.html) for inheritance examples.


> **Warning:** If a callable filter raises an exception itself (e.g., accessing a missing attribute), it is logged as a warning and treated as non-matching — it will **not** propagate.

### Exception Handling Outcomes

| Scenario | Result |
|---|---|
| Exception class (or parent) is in `exceptions_dict` and a filter matches (string substring, callable returns truthy, or filter list is empty) | Exception is **ignored**; sampler sleeps and retries. |
| Exception class is in `exceptions_dict` but **no** filter matches | `TimeoutExpiredError` is raised **immediately**. |
| Exception class is **not** in `exceptions_dict` (and no parent class is listed) | `TimeoutExpiredError` is raised **immediately**. |
| No exception; timeout expires | `TimeoutExpiredError` is raised after the loop ends. |

### Example — Ignore Specific Exceptions

```python
from timeout_sampler import TimeoutSampler

for sample in TimeoutSampler(
    wait_timeout=30,
    sleep=2,
    func=my_flaky_func,
    exceptions_dict={ConnectionError: [], TimeoutError: []},
):
    if sample:
        break
```

### Example — Filter by Exception Message

```python
from timeout_sampler import TimeoutSampler

for sample in TimeoutSampler(
    wait_timeout=30,
    sleep=2,
    func=my_func,
    exceptions_dict={ValueError: ["not ready", "try again"]},
):
    if sample:
        break
```

A `ValueError("resource not ready")` is ignored (contains `"not ready"`). A `ValueError("invalid input")` causes an immediate `TimeoutExpiredError`.

### Example — Callable Filter

```python
from timeout_sampler import TimeoutSampler

# Only retry on HTTP 5xx errors; 4xx errors raise immediately.
for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=1,
    func=make_request,
    exceptions_dict={HttpError: [lambda exc: exc.status >= 500]},
):
    if sample:
        break
```

### Example — Mixed String and Callable Filters

```python
from timeout_sampler import TimeoutSampler

# Retry if message contains "connection refused" OR status >= 500.
for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=1,
    func=make_request,
    exceptions_dict={HttpError: ["connection refused", lambda exc: exc.status >= 500]},
):
    if sample:
        break
```

---

## Internal Methods

These methods are not part of the public API but are documented for contributor reference.

### `_validate_exceptions_dict`

```python
@staticmethod
_validate_exceptions_dict(exceptions_dict: ExceptionsDict) -> ExceptionsDict
```

Validates and returns a defensive copy of `exceptions_dict`. Called during `__init__`.

**Raises:** `TypeError` if keys aren't `Exception` subclasses, values aren't lists, or filter items aren't non-empty strings or callables. Passing a class (e.g., `ValueError`) as a filter item instead of a callable also raises `TypeError`.

### `_is_exception_matched`

```python
@staticmethod
_is_exception_matched(exp: Exception, exception_filters: list[ExceptionFilter]) -> bool
```

| Parameter | Type | Description |
|---|---|---|
| `exp` | `Exception` | The exception instance raised by `func`. |
| `exception_filters` | `list[ExceptionFilter]` | List of allowed filters — strings (substring match against `str(exp)`) or callables (invoked with `exp`, returning truthy to match). Empty list matches everything. |

**Returns:** `True` if `exception_filters` is empty, if any string in the list is a substring of `str(exp)`, or if any callable returns a truthy value when called with `exp`. `False` otherwise.

> **Note:** If a callable filter raises an exception when invoked, a warning is logged and the filter is treated as non-matching.

### `_should_ignore_exception`

```python
_should_ignore_exception(self, exp: Exception) -> bool
```

| Parameter | Type | Description |
|---|---|---|
| `exp` | `Exception` | The exception instance raised by `func`. |

**Returns:** `True` if the exception should be **ignored** (matches an entry in `exceptions_dict` via `isinstance()` and message filtering). `False` if the exception should be re-raised.

### `_get_func_info`

```python
_get_func_info(self, _func: Callable, type_: str) -> Any
```

Resolves function metadata (`__module__`, `__name__`) for regular, `partial`, and `lambda` functions. Used internally to build log messages.

### `_redact`

```python
_redact(self, data: Any, _depth: int = 0) -> Any
```

Recursively redacts values whose keys exactly match sensitive keys (case-insensitive). Traverses dicts, lists, and tuples up to `_MAX_REDACT_DEPTH` (20) levels. Values for matching keys are replaced with `"***"`.

### `_func_log` (cached property)

```python
@functools.cached_property
_func_log(self) -> str
```

**Returns:** A formatted string describing the function call, e.g. `"Function: mymodule.my_func Args: (1, 2) Kwargs: {'key': 'val'}"`. Controlled by `print_func_log` and `print_func_args`. Sensitive values in args and kwargs are redacted.

### `_get_exception_log`

```python
_get_exception_log(self, exp: Exception | None = None) -> str
```

| Parameter | Type | Description |
|---|---|---|
| `exp` | `Exception \| None` | The last exception raised, or `None` if no exception occurred. |

**Returns:** A multi-line string containing the timeout value, function info (if `print_func_log` is `True`), and the last exception class name and message. This string becomes the `value` attribute of the raised `TimeoutExpiredError`.

---

## Raised Exceptions

`TimeoutSampler` raises only one exception type: [`TimeoutExpiredError`](api-exceptions.html).

| Condition | `last_exp` | `elapsed_time` |
|---|---|---|
| Timeout expires with no exception from `func` | `None` | `None` |
| Timeout expires after ignored exceptions | Last ignored `Exception` instance | `None` |
| Exception not matched by `exceptions_dict` | The unmatched `Exception` instance | Seconds elapsed at time of exception |

See [TimeoutExpiredError Reference](api-exceptions.html) for the full attribute and string-representation reference.

---

## Sensitive Key Redaction

When logging function arguments, `TimeoutSampler` automatically redacts values for keys that match known sensitive names. Matching is **case-insensitive** and **exact** (e.g., `"token"` matches a key named `"Token"` or `"TOKEN"` but **not** `"nextPageToken"`).

### Default Sensitive Keys

`authorization`, `token`, `access_token`, `password`, `secret`, `api_key`, `apikey`

### Adding Custom Keys

Pass the `sensitive_keys` parameter to merge additional keys with the defaults:

```python
for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=1,
    func=call_api,
    sensitive_keys=frozenset({"x-custom-secret"}),
    headers={"Authorization": "Bearer token", "x-custom-secret": "value"}, # pragma: allowlist secret
):
    if sample:
        break
# Log output: Kwargs: {'headers': {'Authorization': '***', 'x-custom-secret': '***'}}
```

Redaction traverses dicts, lists, and tuples recursively up to 20 levels deep. Data nested beyond this limit is replaced with `"<redacted: max depth exceeded>"`.

> **Note:** Passing an empty `frozenset()` as `sensitive_keys` still uses all default sensitive keys. To redact **only** defaults, omit the parameter entirely.


> **Warning:** All elements of `sensitive_keys` must be strings. Passing non-string elements (e.g., `int`, `None`) raises `TypeError` at construction time.

---

## Logging Behavior

Logging is emitted via `simple_logger` at `INFO` level.

| Flag | Default | Effect when `True` |
|---|---|---|
| `print_log` | `True` | Logs a startup message with wait/sleep times and logs elapsed time after each iteration where `func` raises an exception or after yield. |
| `print_func_log` | `True` | Appends function module and name to the startup log message. Requires `print_log=True`. |
| `print_func_args` | `True` | Includes `Args` and `Kwargs` in the function log (with sensitive values redacted). Requires `print_func_log=True`. |

See [Controlling Log Output](controlling-logging.html) for usage examples and sample output.

---

## Import Path

```python
from timeout_sampler import TimeoutSampler
```

The class is exported from the top-level `timeout_sampler` package (`timeout_sampler/__init__.py`).

---

## Type Aliases

The following public type aliases are used in the `TimeoutSampler` API and are re-exported from the `timeout_sampler` package:

```python
from timeout_sampler import ExceptionFilter, ExceptionsDict
```

| Alias | Definition | Description |
|---|---|---|
| `ExceptionFilter` | `str \| Callable[[Exception], bool]` | A single filter item — either a substring to match against `str(exception)` or a callable that receives the exception and returns truthy to ignore. |
| `ExceptionsDict` | `dict[type[Exception], list[ExceptionFilter]]` | Mapping of exception classes to their filter lists. Used as the type for the `exceptions_dict` parameter. |

## Related Pages

- [Polling a Function with TimeoutSampler](polling-with-timeout-sampler.html)
- [How Exception Matching Works](exception-matching-logic.html)
- [TimeoutExpiredError Reference](api-exceptions.html)
- [Controlling Log Output](controlling-logging.html)
- [@retry Decorator API](api-retry-decorator.html)

---

Source: api-retry-decorator.md

# @retry Decorator API

The `retry` decorator wraps a function so it is automatically polled via [`TimeoutSampler`](api-timeout-sampler.html) until it returns a truthy value or the timeout expires.

## Import

```python
from timeout_sampler import retry
```

## Signature

```python
def retry(
    wait_timeout: int,
    sleep: int,
    exceptions_dict: dict[type[Exception], list[str | Callable[[Exception], bool]]] | None = None,
    print_log: bool = True,
    print_func_log: bool = True,
    print_func_args: bool = True,
    sensitive_keys: frozenset[str] | set[str] | None = None,
) -> Callable
```

## Parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `wait_timeout` | `int` | *(required)* | Maximum time in seconds to keep retrying the decorated function. |
| `sleep` | `int` | *(required)* | Time in seconds to wait between each call to the decorated function. |
| `exceptions_dict` | `dict[type[Exception], list[str \| Callable[[Exception], bool]]] \| None` | `None` | Exception filter map. Values can be substring strings or callables that receive the exception and return truthy to ignore. When `None`, defaults to `{Exception: []}` (all exceptions ignored). See [How Exception Matching Works](exception-matching-logic.html). |
| `print_log` | `bool` | `True` | When `True`, logs elapsed time and timeout configuration. See [Controlling Log Output](controlling-logging.html). |
| `print_func_log` | `bool` | `True` | When `True`, includes function module and name in log output. |
| `print_func_args` | `bool` | `True` | When `True` (and `print_func_log` is also `True`), includes function arguments and keyword arguments in log output. |
| `sensitive_keys` | `frozenset[str] \| set[str] \| None` | `None` | Additional keys to redact from logged kwargs (case-insensitive exact match). Merged with the built-in default sensitive keys (`authorization`, `token`, `access_token`, `password`, `secret`, `api_key`, `apikey`). |

## Parameter Mapping to TimeoutSampler

Every `@retry` parameter maps directly to a [`TimeoutSampler`](api-timeout-sampler.html) constructor parameter of the same name. The decorator also forwards the decorated function's positional arguments as `func_args` and keyword arguments as `**func_kwargs`.

| `@retry` parameter | `TimeoutSampler` parameter |
|---|---|
| `wait_timeout` | `wait_timeout` |
| `sleep` | `sleep` |
| `exceptions_dict` | `exceptions_dict` |
| `print_log` | `print_log` |
| `print_func_log` | `print_func_log` |
| `print_func_args` | `print_func_args` |
| `sensitive_keys` | `sensitive_keys` |
| *(decorated function)* | `func` |
| *(positional args at call time)* | `func_args` |
| *(keyword args at call time)* | `**func_kwargs` |

## Return Value

The decorator returns the first **truthy** value returned by the decorated function. If the function never returns a truthy value within `wait_timeout` seconds, a [`TimeoutExpiredError`](api-exceptions.html) is raised.

> **Note:** A return value of `False`, `None`, `0`, `""`, `[]`, `{}`, or any other falsy value is treated as a failed attempt and triggers another retry. Only truthy values cause `@retry` to stop and return.

## Exceptions

| Exception | Condition |
|---|---|
| [`TimeoutExpiredError`](api-exceptions.html) | Raised when the decorated function does not return a truthy value within `wait_timeout` seconds, or when an unmatched exception is raised by the function. |

> **Warning:** When `exceptions_dict` is `None` (the default), the internal `TimeoutSampler` uses `{Exception: []}`, which silently catches **all** exceptions during polling. Pass an explicit empty dict `{}` to let every exception propagate immediately.

## Examples

### Basic Usage

```python
from timeout_sampler import retry

@retry(wait_timeout=30, sleep=5)
def wait_for_service():
    response = requests.get("http://localhost:8080/health")
    return response.status_code == 200

# Polls every 5 seconds for up to 30 seconds.
# Returns True on success, raises TimeoutExpiredError on timeout.
wait_for_service()
```

### With Arguments

Arguments passed at call time are forwarded to the decorated function:

```python
from timeout_sampler import retry

@retry(wait_timeout=10, sleep=2)
def check_status(host, port, path="/health"):
    response = requests.get(f"http://{host}:{port}{path}")
    return response.ok

# 'host' and 'port' are forwarded as func_args;
# 'path' is forwarded as a keyword argument.
check_status("localhost", 8080, path="/ready")
```

### Filtering Specific Exceptions

```python
from timeout_sampler import retry

@retry(
    wait_timeout=20,
    sleep=3,
    exceptions_dict={ConnectionError: [], TimeoutError: ["timed out"]},
)
def fetch_data():
    return requests.get("http://api.example.com/data").json()

# ConnectionError with any message is ignored during polling.
# TimeoutError is ignored only if its message contains "timed out".
# All other exceptions propagate immediately.
result = fetch_data()
```

### Callable Exception Filters

Filter values can be callables that receive the exception instance and return a truthy value to ignore (retry):

```python
from timeout_sampler import retry

@retry(
    wait_timeout=60,
    sleep=1,
    exceptions_dict={HttpError: [lambda exc: exc.status >= 500]},
)
def make_request():
    return requests.get("http://api.example.com/data").json()

# Only retries on HTTP 5xx errors; 4xx errors propagate immediately.
result = make_request()
```

Callable and string filters can be combined in the same list:

```python
@retry(
    wait_timeout=60,
    sleep=1,
    exceptions_dict={HttpError: ["connection refused", lambda exc: exc.status >= 500]},
)
def make_request():
    return requests.get("http://api.example.com/data").json()
```

See [Filtering and Handling Exceptions](handling-exceptions.html) for the full exception matching semantics.

### Suppressing Log Output

```python
from timeout_sampler import retry

@retry(wait_timeout=5, sleep=1, print_log=False)
def quiet_check():
    return some_condition()
```

### Redacting Sensitive Keys

```python
from timeout_sampler import retry

@retry(wait_timeout=30, sleep=2, sensitive_keys=frozenset({"x-custom-secret"}))
def call_api(headers):
    return requests.get("http://api.example.com", headers=headers).json()

# Keys like 'authorization', 'token', 'password' are redacted by default.
# 'x-custom-secret' is added to the redaction list.
call_api(headers={"Authorization": "Bearer tok", "x-custom-secret": "val"}) # pragma: allowlist secret
# Log output shows: {'Authorization': '***', 'x-custom-secret': '***'}
```

### Returning a Non-Boolean Truthy Value

```python
from timeout_sampler import retry

@retry(wait_timeout=15, sleep=2)
def get_items():
    items = fetch_items_from_queue()
    return items  # Returns the list when non-empty; retries on empty list

result = get_items()  # result is the first non-empty list returned
```

### Handling TimeoutExpiredError

```python
from timeout_sampler import retry, TimeoutExpiredError

@retry(wait_timeout=5, sleep=1)
def unreliable():
    return False

try:
    unreliable()
except TimeoutExpiredError as e:
    print(f"Gave up after {e.elapsed_time}s")
    print(f"Last exception: {e.last_exp}")
```

See [TimeoutExpiredError Reference](api-exceptions.html) for all available attributes on the exception.

## Related Pages

- [Retrying Functions with the @retry Decorator](using-the-retry-decorator.html)
- [TimeoutSampler API](api-timeout-sampler.html)
- [Filtering and Handling Exceptions](handling-exceptions.html)
- [Controlling Log Output](controlling-logging.html)
- [TimeoutExpiredError Reference](api-exceptions.html)

---

Source: api-timeout-watch.md

# TimeoutWatch API

## Overview

`TimeoutWatch` is a lightweight time-tracking class that records a start time on creation and computes remaining time on demand. It is used internally by [`TimeoutSampler`](api-timeout-sampler.html) and can be used independently in custom polling or orchestration workflows.

## Import

```python
from timeout_sampler import TimeoutWatch
```

## Class: `TimeoutWatch`

```python
class TimeoutWatch(timeout: float) -> None
```

A time counter that determines the time remaining since the start of a given interval. The clock starts immediately upon construction.

---

### Constructor

```python
TimeoutWatch(timeout: float)
```

Creates a new `TimeoutWatch` instance. Records the current time as the start time and stores the specified timeout duration.

#### Parameters

| Name      | Type    | Default | Description                                      |
|-----------|---------|---------|--------------------------------------------------|
| `timeout` | `float` | —       | Duration of the interval in seconds to track.     |

#### Attributes Set

| Attribute    | Type    | Description                                              |
|--------------|---------|----------------------------------------------------------|
| `timeout`    | `float` | The timeout duration passed to the constructor.           |
| `start_time` | `float` | The wall-clock time (`time.time()`) captured at creation. |

#### Example

```python
from timeout_sampler import TimeoutWatch

watch = TimeoutWatch(timeout=30)
print(watch.timeout)      # 30
print(watch.start_time)   # e.g. 1750600000.123456
```

---

### Method: `remaining_time`

```python
remaining_time() -> int | float
```

Returns the number of seconds remaining in the timeout interval, calculated as:

```
max(0, start_time + timeout - current_time)
```

The return value never goes below `0`.

#### Parameters

None.

#### Return Value

| Type          | Description                                                                 |
|---------------|-----------------------------------------------------------------------------|
| `int \| float` | Seconds remaining. Returns `0` (or `0.0`) once the timeout has elapsed.    |

#### Example

```python
import time
from timeout_sampler import TimeoutWatch

watch = TimeoutWatch(timeout=5)

time.sleep(2)
print(watch.remaining_time())  # ≈ 3.0

time.sleep(4)
print(watch.remaining_time())  # 0
```

#### Use in a Custom Polling Loop

```python
import time
from timeout_sampler import TimeoutWatch

watch = TimeoutWatch(timeout=10)

while watch.remaining_time() > 0:
    result = check_some_condition()
    if result:
        break
    time.sleep(1)
else:
    raise RuntimeError("Condition not met within 10 seconds")
```

> **Note:** `remaining_time()` is guaranteed to return `0` (never a negative value) once the timeout has elapsed. You can safely use `> 0` as the loop condition.

---

## Relationship to TimeoutSampler

`TimeoutSampler` creates a `TimeoutWatch` internally to manage its iteration deadline. If you need a polling loop with built-in exception handling and logging, use [`TimeoutSampler`](api-timeout-sampler.html) instead. Use `TimeoutWatch` directly when you need manual control over the polling logic.

For a usage-oriented walkthrough, see [Tracking Elapsed Time with TimeoutWatch](tracking-elapsed-time.html).

---

## Computing Elapsed Time

`TimeoutWatch` does not provide a dedicated elapsed-time method. Compute it by subtracting the remaining time from the original timeout:

```python
from timeout_sampler import TimeoutWatch

watch = TimeoutWatch(timeout=30)

# ... some work ...

elapsed = watch.timeout - watch.remaining_time()
print(f"Elapsed: {elapsed:.2f}s")
```

> **Tip:** This is the same pattern [`TimeoutSampler`](api-timeout-sampler.html) uses internally to populate the `elapsed_time` attribute on [`TimeoutExpiredError`](api-exceptions.html).

## Related Pages

- [Tracking Elapsed Time with TimeoutWatch](tracking-elapsed-time.html)
- [TimeoutSampler API](api-timeout-sampler.html)
- [TimeoutExpiredError Reference](api-exceptions.html)
- [Polling a Function with TimeoutSampler](polling-with-timeout-sampler.html)
- [Common Polling Patterns](common-polling-patterns.html)

---

Source: api-exceptions.md

# TimeoutExpiredError Reference

`TimeoutExpiredError` is the exception raised by [`TimeoutSampler`](api-timeout-sampler.html) and the [`@retry` decorator](api-retry-decorator.html) when the polled function does not produce a truthy result within the specified timeout, or when a raised exception is not matched by the configured `exceptions_dict`.

## Import

```python
from timeout_sampler import TimeoutExpiredError
```

## Class Signature

```python
class TimeoutExpiredError(Exception):
    def __init__(
        self,
        value: str,
        last_exp: Exception | None = None,
        elapsed_time: float | None = None,
    ) -> None: ...
```

`TimeoutExpiredError` is a direct subclass of `Exception`.

## Constructor Parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `value` | `str` | *(required)* | Message describing the timeout context. Includes timeout duration, function info, and the last exception name/text. |
| `last_exp` | `Exception \| None` | `None` | The last exception caught during polling, if any. `None` when the function returned without raising but never produced a truthy result. |
| `elapsed_time` | `float \| None` | `None` | Seconds elapsed from the start of polling until the error was raised. `None` when not tracked (e.g., on a natural timeout expiry at the end of the iteration loop). |

## Instance Attributes

| Attribute | Type | Description |
|---|---|---|
| `value` | `str` | The descriptive message passed to the constructor. |
| `last_exp` | `Exception \| None` | Reference to the last exception raised by the polled function. Useful for inspecting the root cause of a timeout. |
| `elapsed_time` | `float \| None` | Wall-clock seconds elapsed since polling started. Present when the timeout was triggered mid-iteration; `None` when the timeout watch expired naturally at the loop boundary. |

### Accessing `last_exp`

```python
from timeout_sampler import TimeoutSampler, TimeoutExpiredError

try:
    for sample in TimeoutSampler(
        wait_timeout=5,
        sleep=1,
        func=my_flaky_function,
        exceptions_dict={ConnectionError: []},
    ):
        if sample:
            break
except TimeoutExpiredError as exp:
    if exp.last_exp is not None:
        print(f"Root cause: {type(exp.last_exp).__name__}: {exp.last_exp}")
    else:
        print("Function never raised, but never returned truthy either")
```

### Accessing `elapsed_time`

```python
from timeout_sampler import TimeoutSampler, TimeoutExpiredError

try:
    for sample in TimeoutSampler(
        wait_timeout=30,
        sleep=2,
        func=check_service_health,
    ):
        if sample:
            break
except TimeoutExpiredError as exp:
    if exp.elapsed_time is not None:
        print(f"Failed after {exp.elapsed_time:.2f} seconds")
```

## String Representation (`__str__`)

`str(exp)` returns a formatted message. The format depends on whether `elapsed_time` is set.

**Without `elapsed_time`:**

```
Timed Out: <value>.
```

**With `elapsed_time`:**

```
Timed Out: <value>.
Elapsed time: <seconds> [<H:MM:SS>]
```

The elapsed-time line uses `datetime.timedelta` for the human-readable duration.

### Example Output

```python
from timeout_sampler import TimeoutExpiredError

# Minimal
err = TimeoutExpiredError(value="10")
print(str(err))
# Timed Out: 10.

# With elapsed time
err = TimeoutExpiredError(value="10", elapsed_time=7.53)
print(str(err))
# Timed Out: 10.
# Elapsed time: 7.53 [0:00:07.530000]
```

> **Note:** When `TimeoutExpiredError` is raised by `TimeoutSampler`, the `value` string contains multiple lines with the timeout duration, function info, and last exception details. The exact format is an internal detail of `TimeoutSampler._get_exception_log()`. See [TimeoutSampler API](api-timeout-sampler.html) for iteration behavior.

### Realistic `str()` from `TimeoutSampler`

When `TimeoutSampler` raises `TimeoutExpiredError`, the string representation typically looks like:

```
Timed Out: 5
Function: my_module.check_service_health
Last exception: ConnectionError: connection refused.
Elapsed time: 4.02 [0:00:04.020000]
```

## When `elapsed_time` Is Set vs. `None`

| Scenario | `elapsed_time` | `last_exp` |
|---|---|---|
| Exception raised mid-iteration that is **not** matched by `exceptions_dict` | Set (seconds since start) | The unmatched exception |
| Matched exception keeps being raised until timeout expires naturally | `None` | The last matched exception |
| Function returns a non-truthy value until timeout expires naturally | `None` | `None` |

> **Tip:** To guarantee `elapsed_time` is always available in your error handling, check for `None` before using it in arithmetic or formatting.

## Catching `TimeoutExpiredError`

`TimeoutExpiredError` can be caught as itself or as its parent `Exception`:

```python
from timeout_sampler import TimeoutExpiredError

# Specific catch
try:
    for sample in sampler:
        if sample:
            break
except TimeoutExpiredError:
    print("Polling timed out")

# Broader catch (also works)
try:
    for sample in sampler:
        if sample:
            break
except Exception as e:
    if isinstance(e, TimeoutExpiredError):
        print(f"Timeout with last_exp={e.last_exp}")
```

## Constructing Manually

You can construct `TimeoutExpiredError` directly for testing or custom polling logic:

```python
from timeout_sampler import TimeoutExpiredError

# Simulate a timeout with a root cause
root_cause = ConnectionError("connection refused")
err = TimeoutExpiredError(
    value="30",
    last_exp=root_cause,
    elapsed_time=29.87,
)

assert err.value == "30"
assert err.last_exp is root_cause
assert err.elapsed_time == 29.87
assert "Timed Out: 30." in str(err)
assert "Elapsed time: 29.87" in str(err)
```

## Related Pages

- [TimeoutSampler API](api-timeout-sampler.html) — constructor parameters and iteration behavior that produce `TimeoutExpiredError`
- [@retry Decorator API](api-retry-decorator.html) — decorator that raises `TimeoutExpiredError` on timeout
- [Filtering and Handling Exceptions](handling-exceptions.html) — configuring `exceptions_dict` to control which exceptions trigger an immediate `TimeoutExpiredError` vs. being silently retried
- [How Exception Matching Works](exception-matching-logic.html) — the algorithm that determines whether an exception is matched or causes `TimeoutExpiredError`
- [Tracking Elapsed Time with TimeoutWatch](tracking-elapsed-time.html) — using `TimeoutWatch` to track elapsed time during polling

## Related Pages

- [How Exception Matching Works](exception-matching-logic.html)
- [Filtering and Handling Exceptions](handling-exceptions.html)
- [TimeoutSampler API](api-timeout-sampler.html)
- [@retry Decorator API](api-retry-decorator.html)
- [Tracking Elapsed Time with TimeoutWatch](tracking-elapsed-time.html)

---

Source: exception-matching-logic.md

# How Exception Matching Works

When your polled function raises an exception inside a `TimeoutSampler` loop, the sampler must decide: *should it swallow the error and keep retrying, or should it stop immediately?* This decision is made by the **exception matching algorithm** — an inheritance-aware, message-filtered check that gives you precise control over which failures are retried and which are surfaced right away.

Understanding this algorithm helps you avoid two common pitfalls: accidentally retrying an exception you should have surfaced (hiding real bugs), or accidentally re-raising a transient error you meant to ignore (breaking your polling loop too early).

## The Big Picture

Every time an exception is raised inside the function passed to `TimeoutSampler`, the sampler runs through a two-stage decision process:

| Stage | What It Checks | Outcome |
|-------|---------------|---------|
| **1. Type matching** | Is the raised exception an instance of any class listed in `exceptions_dict`? This uses Python's `isinstance()`, so subclass relationships are honored. | If no match → **re-raise immediately** |
| **2. Filter matching** | Does the exception pass at least one filter in the matched class's filter list? Filters can be **substring strings** (checked against `str(exception)`) or **callables** (invoked with the exception, returning a truthy value to ignore). | If match → **ignore and retry**; if no filter match → **re-raise immediately** |

If the exception passes both stages, the sampler sleeps and calls the function again. If it fails either stage, the sampler wraps the original exception in a `TimeoutExpiredError` and raises it.

## The Three Outcome Categories

When an exception is raised inside your polled function, exactly one of these three things happens:

### 1. Exact Class Match — Continue Polling

The raised exception's class is explicitly listed as a key in `exceptions_dict`, and the message filter passes (or is empty).

```python
from timeout_sampler import TimeoutSampler

# ValueError is explicitly listed, empty list means "match any message"
exceptions_dict = {ValueError: []}

for sample in TimeoutSampler(
    wait_timeout=10,
    sleep=1,
    func=might_raise_value_error,
    exceptions_dict=exceptions_dict,
):
    if sample:
        break
# Any ValueError is silently retried until timeout
```

### 2. Inherited Class Match — Continue Polling

The raised exception is a *subclass* of a class listed in `exceptions_dict`. The sampler uses `isinstance()` internally, so the full inheritance chain is checked.

```python
# Imagine this hierarchy:
# class AExampleError(Exception): ...
# class BExampleError(AExampleError): ...

exceptions_dict = {AExampleError: []}

# If the function raises BExampleError, it still matches
# because isinstance(BExampleError(), AExampleError) is True
```

### 3. No Match — Re-raise Immediately

The raised exception is neither listed in `exceptions_dict` nor a subclass of any listed class. The sampler wraps it in a `TimeoutExpiredError` and re-raises immediately — it does *not* wait for the timeout to expire.

```python
exceptions_dict = {ValueError: []}

# If the function raises KeyError, it does NOT match ValueError
# and is NOT a subclass of ValueError → re-raised immediately
```

> **Warning:** If you pass an empty `exceptions_dict` (`{}`), **no exceptions will be matched**, so *every* exception will cause an immediate re-raise. This is different from the default behavior (see below).

## How Filter Matching Works

Each key in `exceptions_dict` maps to a list of **filters**. A filter can be a **string** (substring match against `str(exception)`) or a **callable** (invoked with the exception instance, returning a truthy value to ignore). The filter list is evaluated *after* the type match succeeds:

| Filter list value | Behavior |
|---|---|
| `[]` (empty list) | **All exceptions match.** Any exception of this type is ignored. |
| `["connection refused", "timeout"]` | The exception's `str()` representation must contain at least one of these substrings. |
| `[lambda exc: exc.status >= 500]` | The callable is invoked with the exception; a truthy return value means the exception is ignored. |
| `["connection refused", lambda exc: exc.status >= 500]` | **Strings and callables can be combined.** The exception is ignored if *any* filter matches. |

> **Note:** Empty strings in the filter list are rejected at construction time with a `TypeError`. If you need to match all messages of a given type, use an empty list `[]` instead.

### String Filters

String filters perform a substring check using Python's `in` operator:

```python
# Internal logic (simplified) for a string filter:
filter_item in str(exp)
```

```python
from timeout_sampler import TimeoutSampler

# Match only ConnectionError with "refused" in the message
exceptions_dict = {ConnectionError: ["refused"]}

# ✅ ConnectionError("Connection refused by host")  → retried (contains "refused")
# ❌ ConnectionError("DNS resolution failed")       → re-raised (no substring match)
# ❌ ValueError("Connection refused")               → re-raised (wrong type)
```

```python
# Match ValueError with ANY of several messages
exceptions_dict = {ValueError: ["not ready", "still loading"]}

# ✅ ValueError("Resource not ready")     → retried
# ✅ ValueError("Page still loading")     → retried
# ❌ ValueError("Invalid input")          → re-raised
```

> **Tip:** String filters are case-sensitive. `"Refused"` will not match an exception with the message `"connection refused"`. Choose your substrings carefully.

### Callable Filters

Callable filters receive the exception instance as their single argument and return a truthy value to indicate the exception should be ignored (retried). This lets you filter based on exception attributes, not just the message string.

```python
# Only retry HttpError when the status code indicates a server error
exceptions_dict = {HttpError: [lambda exc: exc.status >= 500]}

# ✅ HttpError(status=503)  → retried (callable returns True)
# ❌ HttpError(status=404)  → re-raised (callable returns False)
```

```python
# Combine a string filter with a callable filter — either can match
exceptions_dict = {HttpError: ["connection refused", lambda exc: exc.status >= 500]}

# ✅ HttpError("connection refused", status=0)   → retried (string matches)
# ✅ HttpError("server error", status=502)        → retried (callable matches)
# ❌ HttpError("not found", status=404)           → re-raised (neither matches)
```

> **Warning:** If a callable filter raises an exception itself, the error is logged as a warning and the filter is treated as non-matching. The sampler continues evaluating the remaining filters in the list.

## The Default `exceptions_dict`

If you do not pass an `exceptions_dict` to `TimeoutSampler`, the default value is:

```python
{Exception: []}
```

Since every exception in Python inherits from `Exception`, this means **all exceptions are silently retried** until the timeout expires. This is the most permissive setting.

```python
# These two are equivalent:
TimeoutSampler(wait_timeout=10, sleep=1, func=my_func)
TimeoutSampler(wait_timeout=10, sleep=1, func=my_func, exceptions_dict={Exception: []})
```

> **Note:** The `@retry` decorator also defaults to `{Exception: []}` when `exceptions_dict` is not specified. See [@retry Decorator API](api-retry-decorator.html) for the full parameter list.

## Step-by-Step: What Happens When an Exception Is Raised

1. Your function (`func`) raises an exception `exp`.
2. The sampler records `exp` as `last_exp` and calculates `elapsed_time`.
3. The sampler calls `_should_ignore_exception(exp)`, which iterates over every key in `exceptions_dict`:
   - For each key class, it checks `isinstance(exp, key)`.
   - On a type match, it retrieves the filter list and calls `_is_exception_matched(exp, filters)`.
   - If both type and filter match → return `True` (ignore the exception).
   - If the type matches but the filter does not, the sampler **continues** checking subsequent entries in the dict.
4. **If ignored:** the sampler sleeps for `sleep` seconds, then calls `func` again.
5. **If not ignored:** the sampler raises `TimeoutExpiredError`, attaching `exp` as `last_exp` and the current `elapsed_time`.

> **Note:** When an exception is not matched, the `TimeoutExpiredError` is raised **immediately** — the sampler does not wait for the full timeout to expire. This means unrecognized exceptions surface fast.

## Multiple Exception Classes

You can list multiple exception classes in `exceptions_dict`. The sampler checks them in iteration order:

```python
exceptions_dict = {
    ConnectionError: ["refused", "reset"],
    TimeoutError: [],
    ValueError: ["not ready"],
}
```

The sampler checks entries in iteration order. For each entry, it tests both `isinstance()` and the filter list together. If a type matches but the filter list does not pass, the sampler **continues** to the next entry in the dict. The exception is only re-raised if *no* entry matches on both type and filters.

> **Warning:** Because `isinstance()` honors inheritance, ordering can matter when your exception classes share a parent-child relationship. If both `AExampleError` and `BExampleError(AExampleError)` are in the dict, the one that appears first during iteration will be checked first. Place more specific (child) classes before more general (parent) classes to ensure the correct message filter is applied.

## How It Affects `TimeoutExpiredError`

When an exception is re-raised (either immediately or at timeout expiry), it is wrapped in a `TimeoutExpiredError`. The original exception is accessible through the `last_exp` attribute:

```python
from timeout_sampler import TimeoutExpiredError, TimeoutSampler

try:
    for sample in TimeoutSampler(
        wait_timeout=5,
        sleep=1,
        func=my_unstable_func,
        exceptions_dict={ConnectionError: []},
    ):
        if sample:
            break
except TimeoutExpiredError as e:
    print(e.last_exp)       # The original exception (e.g., ConnectionError)
    print(e.elapsed_time)   # Seconds elapsed before the error
```

See [TimeoutExpiredError Reference](api-exceptions.html) for the full attribute and method reference.

## Quick Reference Table

| Scenario | `exceptions_dict` | Raised Exception | Result |
|---|---|---|---|
| Default — catch all | `{Exception: []}` | Any exception | Retry until timeout |
| Specific type, any message | `{ValueError: []}` | `ValueError("anything")` | Retry |
| Specific type, filtered message | `{ValueError: ["not ready"]}` | `ValueError("not ready yet")` | Retry |
| Specific type, wrong message | `{ValueError: ["not ready"]}` | `ValueError("bad input")` | Re-raise immediately |
| Callable filter match | `{HttpError: [lambda exc: exc.status >= 500]}` | `HttpError(status=503)` | Retry |
| Callable filter no match | `{HttpError: [lambda exc: exc.status >= 500]}` | `HttpError(status=404)` | Re-raise immediately |
| Mixed string + callable | `{HttpError: ["refused", lambda exc: exc.status >= 500]}` | `HttpError("refused", status=0)` | Retry (string matches) |
| Subclass match | `{Exception: []}` | `ValueError()` | Retry (ValueError inherits Exception) |
| Parent does not match child | `{ValueError: []}` | `Exception()` | Re-raise immediately |
| Empty dict — catch nothing | `{}` | Any exception | Re-raise immediately |

## Related Pages

- [Filtering and Handling Exceptions](handling-exceptions.html) — practical guide to configuring `exceptions_dict` for common scenarios
- [TimeoutSampler API](api-timeout-sampler.html) — full constructor parameters and iteration behavior reference
- [TimeoutExpiredError Reference](api-exceptions.html) — attributes and string representation of the error raised on timeout or unmatched exceptions
- [@retry Decorator API](api-retry-decorator.html) — how `exceptions_dict` is passed through the decorator
- [Common Polling Patterns](common-polling-patterns.html) — copy-paste recipes combining exception filters with polling strategies

## Related Pages

- [Filtering and Handling Exceptions](handling-exceptions.html)
- [Using Callable Exception Filters](callable-exception-filters.html)
- [TimeoutSampler API](api-timeout-sampler.html)
- [TimeoutExpiredError Reference](api-exceptions.html)
- [Common Polling Patterns](common-polling-patterns.html)

---

Source: sensitive-key-redaction.md

# Redacting Sensitive Data from Log Output

When polling functions that handle credentials, API keys, or other secrets, you need to ensure those values never leak into your application logs. The `sensitive_keys` parameter lets you control exactly which argument keys are masked in log output from both `TimeoutSampler` and `@retry`.

## Prerequisites

- `timeout-sampler` installed in your project
- Basic familiarity with `TimeoutSampler` or the `@retry` decorator (see [Polling a Function with TimeoutSampler](polling-with-timeout-sampler.html) or [Retrying Functions with the @retry Decorator](using-the-retry-decorator.html))

## Quick Example

```python
from timeout_sampler import TimeoutSampler

for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=5,
    func=call_api,
    headers={"Authorization": "Bearer my-secret-token", "Content-Type": "application/json"},
):
    if sample:
        break
```

The log output automatically masks the `Authorization` value:

```
Kwargs: {'headers': {'Authorization': '***', 'Content-Type': 'application/json'}}
```

No configuration needed — `Authorization` is one of the built-in sensitive keys.

## Built-in Default Sensitive Keys

The following keys are redacted automatically, with no extra configuration:

| Key              | Common Use                          |
|------------------|-------------------------------------|
| `authorization`  | HTTP Authorization headers          |
| `token`          | OAuth/session tokens                |
| `access_token`   | OAuth2 access tokens                |
| `password`       | User/service credentials            |
| `secret`         | Shared secrets                      |
| `api_key`        | API keys                            |
| `apikey`         | API keys (alternate spelling)       |

> **Note:** Matching is **case-insensitive** and uses **exact key name** comparison. A key named `Authorization` or `AUTHORIZATION` will be redacted, but a key like `nextPageToken` or `token_count` will **not** — only a key named exactly `token` (in any case) triggers redaction.

## Adding Custom Sensitive Keys

Pass a `set` or `frozenset` of additional key names to the `sensitive_keys` parameter:

```python
from timeout_sampler import TimeoutSampler

for sample in TimeoutSampler(
    wait_timeout=60,
    sleep=1,
    func=call_api,
    sensitive_keys=frozenset({"x-custom-secret"}),
    headers={"Authorization": "Bearer token", "x-custom-secret": "value"}, # pragma: allowlist secret
):
    if sample:
        break
```

The log output redacts both the built-in key and your custom key:

```
Kwargs: {'headers': {'Authorization': '***', 'x-custom-secret': '***'}}
```

Custom keys are **merged** with the defaults — you never lose the built-in protection. Custom key matching is also case-insensitive: `"X-My-Token"` will match `x-my-token`, `X-MY-TOKEN`, etc.

### Using `sensitive_keys` with `@retry`

The `@retry` decorator accepts the same parameter:

```python
from timeout_sampler import retry

@retry(
    wait_timeout=30,
    sleep=2,
    sensitive_keys=frozenset({"x-api-secret", "session_id"}),
)
def fetch_data(headers=None, session_id=None):
    # Both headers containing default sensitive keys AND session_id will be redacted
    return make_request(headers=headers, session_id=session_id)

fetch_data(
    headers={"Authorization": "Bearer abc123"},
    session_id="sess-xyz-999",
)
```

See [@retry Decorator API](api-retry-decorator.html) for the full parameter reference.

### Passing an Empty Set

Passing an empty `frozenset` or `set` still preserves all built-in defaults — it does not disable redaction:

```python
# Built-in keys are still redacted
sampler = TimeoutSampler(
    wait_timeout=60,
    sleep=1,
    func=call_api,
    sensitive_keys=frozenset(),  # defaults still active
    headers={"Authorization": "Bearer still-redacted"},
)
```

## How Recursive Redaction Works

Redaction isn't limited to top-level keyword arguments. It walks through your data **recursively**, masking sensitive keys inside nested structures:

### Nested Dictionaries

```python
TimeoutSampler(
    wait_timeout=10,
    sleep=1,
    func=process,
    config={"database": {"password": "hunter2", "host": "db.example.com"}}, # pragma: allowlist secret
)
# Logged as: Kwargs: {'config': {'database': {'password': '***', 'host': 'db.example.com'}}}
```

### Lists Containing Dictionaries

```python
TimeoutSampler(
    wait_timeout=10,
    sleep=1,
    func=process,
    args_list=[{"token": "secret-in-list", "id": 42}],
)
# Logged as: Kwargs: {'args_list': [{'token': '***', 'id': 42}]}
```

### Tuples Containing Dictionaries

Tuples are traversed the same way as lists — any dictionary found inside a tuple has its sensitive keys redacted.

### Positional Arguments

Dictionaries passed as positional arguments via `func_args` are also redacted:

```python
TimeoutSampler(
    wait_timeout=10,
    sleep=1,
    func=send_request,
    func_args=({"Authorization": "Bearer pos-secret", "safe": "visible"},),
)
# Logged as: Args: ({'Authorization': '***', 'safe': 'visible'},)
```

### Non-String Dictionary Keys

Dictionaries with non-string keys (integers, tuples, etc.) are handled safely. Only string keys are checked for sensitive matches — non-string keys are passed through unchanged:

```python
TimeoutSampler(
    wait_timeout=10,
    sleep=1,
    func=process,
    data={1: "int-key-value", "password": "secret123"}, # pragma: allowlist secret
)
# Logged as: Kwargs: {'data': {1: 'int-key-value', 'password': '***'}}
```

## Advanced Usage

### Disabling Argument Logging Entirely

If you prefer to suppress all argument output rather than relying on selective redaction, set `print_func_args=False`:

```python
TimeoutSampler(
    wait_timeout=60,
    sleep=5,
    func=call_api,
    print_func_args=False,
    headers={"Authorization": "Bearer secret"},
)
# Log output contains no Args/Kwargs section at all
```

See [Controlling Log Output](controlling-logging.html) for more logging options.

### Depth Limit for Nested Data

Redaction traverses up to **20 levels** of nesting. Data nested beyond this depth is replaced with a sentinel value instead of being logged:

```
<redacted: max depth exceeded>
```

This protects against stack overflows from extremely deep or circular data structures. In practice, 20 levels is far deeper than any typical API payload.

### Type Validation on `sensitive_keys`

The `sensitive_keys` parameter must contain **only strings**. Passing non-string values raises a `TypeError` immediately at construction time:

```python
# Raises TypeError: sensitive_keys must contain only strings, got int: 123
TimeoutSampler(
    wait_timeout=10,
    sleep=1,
    func=call_api,
    sensitive_keys=frozenset({123, "valid_key"}),
)
```

> **Tip:** Validate your `sensitive_keys` values early. Errors are raised at `TimeoutSampler` or `@retry` initialization — not when log output is generated — so you'll catch mistakes before any polling begins.

## Troubleshooting

| Problem | Cause | Solution |
|---------|-------|----------|
| A key like `nextPageToken` is unexpectedly redacted | Custom `sensitive_keys` contains a key that partially matches | Redaction uses **exact** key name matching (case-insensitive). Check your `sensitive_keys` set for overly broad entries. |
| Sensitive values still appear in logs | The key name isn't in the default set or your custom set | Add the key to `sensitive_keys`. Only exact key-name matches are redacted. |
| `TypeError` when constructing the sampler | Non-string value in `sensitive_keys` | Ensure all elements in the set are strings. |
| `<redacted: max depth exceeded>` appears in logs | Data structure is nested more than 20 levels deep | This is expected safety behavior. Restructure data or disable argument logging with `print_func_args=False`. |
| No kwargs visible at all in logs | `print_func_args` is set to `False` | Set `print_func_args=True` (the default) to see redacted argument output. |

## Related Pages

- [Controlling Log Output](controlling-logging.html)
- [TimeoutSampler API](api-timeout-sampler.html)
- [@retry Decorator API](api-retry-decorator.html)
- [Polling a Function with TimeoutSampler](polling-with-timeout-sampler.html)
- [Retrying Functions with the @retry Decorator](using-the-retry-decorator.html)

---

Source: callable-exception-filters.md

# Using Callable Exception Filters

Filter exceptions during polling based on runtime attributes — like HTTP status codes or error categories — by passing callable filters (lambdas or functions) alongside string filters in your `exceptions_dict`.

## Prerequisites

- `timeout-sampler` installed in your project (see [Getting Started with timeout-sampler](quickstart.html))
- Basic familiarity with `exceptions_dict` string-based filtering (see [Filtering and Handling Exceptions](handling-exceptions.html))

## Quick Example

Ignore server errors (status ≥ 500) and retry, but immediately stop on client errors:

```python
from timeout_sampler import TimeoutSampler

for sample in TimeoutSampler(
    wait_timeout=30,
    sleep=2,
    func=call_my_api,
    exceptions_dict={
        HttpError: [lambda exc: exc.status >= 500]
    },
):
    if sample:
        break
```

If `call_my_api` raises an `HttpError` with `status=502`, the sampler retries. If it raises one with `status=404`, polling stops immediately with a `TimeoutExpiredError`.

## Type Aliases

The library exports two type aliases you can use for type-safe exception filter configuration:

```python
from timeout_sampler import ExceptionFilter, ExceptionsDict
```

| Type Alias        | Definition                                        | Purpose                                                   |
|-------------------|---------------------------------------------------|-----------------------------------------------------------|
| `ExceptionFilter` | `str \| Callable[[Exception], bool]`              | A single filter: either a substring match or a callable   |
| `ExceptionsDict`  | `dict[type[Exception], list[ExceptionFilter]]`    | The full mapping passed to `exceptions_dict`              |

Use these to annotate your own helper functions or configuration builders:

```python
from timeout_sampler import ExceptionsDict

def build_api_filters(retryable_codes: list[int]) -> ExceptionsDict:
    return {
        HttpError: [lambda exc: exc.status in retryable_codes]
    }

filters = build_api_filters([500, 502, 503, 504])
```

## How Callable Filters Work

1. Your polled function raises an exception.
2. The sampler checks if the exception's type (or a parent type) is a key in `exceptions_dict`.
3. If the filter list is **empty** (`[]`), all instances of that exception are ignored and the sampler retries.
4. If the filter list contains **callables**, each callable is invoked with the exception instance. If any callable returns a **truthy** value, the exception is ignored and the sampler retries.
5. If no filter matches, polling stops and a `TimeoutExpiredError` is raised.

> **Note:** Filters are evaluated in list order. The sampler stops at the first match — either a string substring match or a callable returning truthy.

## Writing Callable Filters

A callable filter is any function or lambda that:

- Accepts exactly **one argument**: the exception instance
- Returns a **truthy** value to ignore the exception (retry), or **falsy** to stop

### Lambda filters

The most concise option for simple conditions:

```python
exceptions_dict = {
    HttpError: [lambda exc: exc.status >= 500]
}
```

### Named functions

Better for complex logic or reusability:

```python
def is_retryable_error(exc):
    """Retry on server errors and rate limiting."""
    return exc.status >= 500 or exc.status == 429

exceptions_dict = {
    HttpError: [is_retryable_error]
}
```

### Filtering on exception attributes

Callable filters shine when you need to inspect attributes beyond the exception message:

```python
# Retry only on specific error codes
exceptions_dict = {
    DatabaseError: [lambda exc: exc.error_code in ("LOCK_TIMEOUT", "DEADLOCK")]
}

# Retry when a response header says to
exceptions_dict = {
    ApiError: [lambda exc: exc.retry_after is not None]
}
```

## Combining String and Callable Filters

You can mix string and callable filters in the same list. The sampler checks each filter in order and retries on the **first match**:

```python
exceptions_dict = {
    ConnectionError: [
        "Connection refused",                    # string: match against str(exception)
        lambda exc: getattr(exc, "errno", 0) == 104,  # callable: check attribute
    ]
}
```

| Filter type | How it matches                                                    |
|-------------|-------------------------------------------------------------------|
| String      | Checked as a **substring** of `str(exception)`                    |
| Callable    | Called with the exception instance; retries if return is **truthy**|

> **Tip:** Put the most common match first in the list to short-circuit evaluation.

## Using Callable Filters with the `@retry` Decorator

Callable filters work identically with the `@retry` decorator:

```python
from timeout_sampler import retry

@retry(
    wait_timeout=60,
    sleep=5,
    exceptions_dict={
        HttpError: [lambda exc: exc.status >= 500],
        ConnectionError: [],
    },
)
def fetch_data(url):
    return requests.get(url).json()
```

See [Retrying Functions with the @retry Decorator](using-the-retry-decorator.html) for full decorator usage.

## Advanced Usage

### Multiple exception types with different filters

Map different exception classes to different filter strategies in a single `exceptions_dict`:

```python
exceptions_dict = {
    HttpError: [lambda exc: exc.status >= 500],
    ConnectionError: [],                        # retry all connection errors
    TimeoutError: ["read timed out"],           # retry only read timeouts
}
```

### Callable filters with `functools.partial`

Use `functools.partial` to create reusable parameterized filters:

```python
from functools import partial

def status_in_range(exc, low, high):
    return low <= exc.status < high

exceptions_dict = {
    HttpError: [partial(status_in_range, low=500, high=600)]
}
```

### Accessing `last_exp` after timeout

When polling ultimately times out, the `TimeoutExpiredError` carries the last exception on its `last_exp` attribute, so you can inspect which exception caused the final failure:

```python
try:
    for sample in TimeoutSampler(
        wait_timeout=10,
        sleep=2,
        func=call_my_api,
        exceptions_dict={HttpError: [lambda exc: exc.status >= 500]},
    ):
        if sample:
            break
except TimeoutExpiredError as e:
    if e.last_exp:
        print(f"Last error status: {e.last_exp.status}")
```

See [TimeoutExpiredError Reference](api-exceptions.html) for all available attributes.

## Error Handling When a Callable Filter Raises

If your callable filter itself raises an exception (for example, accessing an attribute that doesn't exist), the sampler handles it **safely**:

- The failing filter is **skipped** and treated as non-matching.
- A warning is logged: `Callable filter <filter> raised <error> for <ExceptionType>, treating as non-matching`.
- Evaluation continues with the remaining filters in the list.
- If no other filter matches, the exception is **not ignored** and a `TimeoutExpiredError` is raised.

```python
# This filter accesses .status, but the exception might not have that attribute
exceptions_dict = {
    Exception: [lambda exc: exc.status >= 500]
}
```

If a plain `Exception("something broke")` is raised (no `.status` attribute), the callable filter raises `AttributeError` internally. The sampler logs a warning, skips that filter, and since no filter matched, stops polling immediately.

> **Warning:** The sampler will never propagate an exception raised by a filter callable. It always catches the error, logs it, and moves on. Make sure your filter logic is correct — a broken filter silently becomes a non-match.

## Validation at Initialization

The `exceptions_dict` is validated when you create a `TimeoutSampler` or apply `@retry` — not at polling time. Invalid configurations raise `TypeError` immediately:

| Mistake                              | Error message                                                  |
|--------------------------------------|----------------------------------------------------------------|
| Using a class as a filter item       | `contains a class (ClassName) instead of a callable or string` |
| Using an empty string as a filter    | `contains an empty string`                                     |
| Using a non-callable, non-string     | `expected str or callable`                                     |
| Using a non-Exception class as a key | `must be an Exception subclass`                                |

```python
# ❌ Wrong: passing an exception class as a filter
exceptions_dict = {HttpError: [ValueError]}
# TypeError: contains a class (ValueError) instead of a callable or string.
# Use a lambda (e.g., lambda exc: exc.status >= 500) instead.

# ✅ Right: passing a callable
exceptions_dict = {HttpError: [lambda exc: isinstance(exc.__cause__, ValueError)]}
```

> **Tip:** Early validation means you'll catch configuration mistakes in tests, not in production during a retry loop.

## Troubleshooting

**Filter never matches even though it should**
- Verify the callable receives the correct exception type. Use `type(exc)` in a test filter to confirm.
- Check that your callable returns a **truthy** value (not `None`). A filter that doesn't explicitly `return True` returns `None`, which is falsy.

**Filter always matches when it shouldn't**
- Ensure your callable isn't returning a truthy value by accident. For example, `lambda exc: exc.message` returns the message string, which is truthy for any non-empty message.

**"Callable filter raised..." warning in logs**
- Your filter callable is crashing at runtime. Check for attribute access on exception types that don't have the expected attribute. Use `getattr(exc, "attr", default)` for defensive access.

## Related Pages

- [How Exception Matching Works](exception-matching-logic.html)
- [Filtering and Handling Exceptions](handling-exceptions.html)
- [TimeoutSampler API](api-timeout-sampler.html)
- [Retrying Functions with the @retry Decorator](using-the-retry-decorator.html)
- [TimeoutExpiredError Reference](api-exceptions.html)

---
