Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions clientwright/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
from .core.contracts.adapter import default_deps
from .core.contracts.settings import client_config_from_settings
from .core.errors import (
AttemptTimeoutError,
CallError,
CircuitOpenError,
ClientwrightError,
Expand Down Expand Up @@ -117,6 +118,7 @@ def build_sync(adapter: str, config: ClientConfig, deps: AdapterDeps | None = No
"UNSET",
"AdapterCapabilities",
"AdapterDeps",
"AttemptTimeoutError",
"CallError",
"CallOptions",
"CallerOverride",
Expand Down
10 changes: 9 additions & 1 deletion clientwright/adapters/_httpx_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from ..core.contracts.adapter import AdapterDeps
from ..core.engine.aio import AsyncAttemptEngine
from ..core.engine.sync import SyncAttemptEngine
from ..core.errors import CallError, CircuitOpenError, DeadlineExceededError, TooManyRedirectsError
from ..core.errors import AttemptTimeoutError, CallError, CircuitOpenError, DeadlineExceededError, TooManyRedirectsError
from ..core.model import IDEMPOTENT_METHODS, ConnMetrics, FailureKind, Outcome, RequestInfo, ResolvedTimeouts, origin_of
from ..core.native import accepted_overrides, validate_native
from ..core.plan import CallPlan, ClientHandle, ClientRuntime, compile_plan, register_handle
Expand Down Expand Up @@ -105,6 +105,7 @@ def capabilities_for(adapter: str) -> AdapterCapabilities:
FailureKind.READ_TIMEOUT,
FailureKind.WRITE_TIMEOUT,
FailureKind.POOL_TIMEOUT,
FailureKind.ATTEMPT_TIMEOUT,
FailureKind.TOTAL_TIMEOUT,
FailureKind.CONNECT_ERROR,
FailureKind.TLS_ERROR,
Expand All @@ -120,6 +121,10 @@ def capabilities_for(adapter: str) -> AdapterCapabilities:
collapses={FailureKind.DNS_ERROR: FailureKind.CONNECT_ERROR},
notes={
"deadline_hard": "Hard cancellation on the async client only; the sync client clamps phases (soft).",
"attempt_timeout": (
"Emitted by the async client only; the sync client drops the attempt ceiling, so a stall arrives as "
"read_timeout."
),
"pool_limit_per_host": "Emulated as a per-origin in-flight semaphore; limits requests, not connections.",
"dns_error": f"{adapter} wraps DNS failures into ConnectError; they surface as connect_error.",
"proxy_from_env": "Environment proxies are parsed into mounts; NO_PROXY entries match hosts literally.",
Expand Down Expand Up @@ -173,6 +178,7 @@ def make_error_translator(
circuit_cls: type[CircuitOpenError],
deadline_cls: type[DeadlineExceededError],
redirects_cls: type[TooManyRedirectsError],
attempt_cls: type[AttemptTimeoutError],
) -> Callable[[CallError], BaseException]:
"""Build the CallError -> dual-family translator from the bound classes."""

Expand All @@ -181,6 +187,8 @@ def translate(error: CallError) -> BaseException:
return circuit_cls(error.key, error.retry_after)
if isinstance(error, DeadlineExceededError):
return deadline_cls(error.total)
if isinstance(error, AttemptTimeoutError):
return attempt_cls(error.attempt)
if isinstance(error, TooManyRedirectsError):
return redirects_cls(error.hops)
return error
Expand Down
3 changes: 3 additions & 0 deletions clientwright/adapters/aiohttp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
if TYPE_CHECKING:
from .adapter import AiohttpAdapter as AiohttpAdapter
from .capabilities import CAPABILITIES as CAPABILITIES
from .errors import AiohttpAttemptTimeoutError as AiohttpAttemptTimeoutError
from .errors import AiohttpCircuitOpenError as AiohttpCircuitOpenError
from .errors import AiohttpDeadlineExceededError as AiohttpDeadlineExceededError
from .errors import AiohttpTooManyRedirectsError as AiohttpTooManyRedirectsError
Expand All @@ -22,6 +23,7 @@
_EXPORTS = {
"CAPABILITIES": "capabilities",
"AiohttpAdapter": "adapter",
"AiohttpAttemptTimeoutError": "errors",
"AiohttpCircuitOpenError": "errors",
"AiohttpDeadlineExceededError": "errors",
"AiohttpTooManyRedirectsError": "errors",
Expand All @@ -37,6 +39,7 @@ def __getattr__(name: str) -> Any:
__all__ = [
"CAPABILITIES",
"AiohttpAdapter",
"AiohttpAttemptTimeoutError",
"AiohttpCircuitOpenError",
"AiohttpDeadlineExceededError",
"AiohttpTooManyRedirectsError",
Expand Down
1 change: 1 addition & 0 deletions clientwright/adapters/aiohttp/capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
{
FailureKind.CONNECT_TIMEOUT,
FailureKind.READ_TIMEOUT,
FailureKind.ATTEMPT_TIMEOUT,
FailureKind.TOTAL_TIMEOUT,
FailureKind.CONNECT_ERROR,
FailureKind.DNS_ERROR,
Expand Down
15 changes: 14 additions & 1 deletion clientwright/adapters/aiohttp/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@

from __future__ import annotations

from ...core.errors import CallError, CircuitOpenError, DeadlineExceededError, TooManyRedirectsError
from ...core.errors import (
AttemptTimeoutError,
CallError,
CircuitOpenError,
DeadlineExceededError,
TooManyRedirectsError,
)
from ._imports import aiohttp


Expand All @@ -18,6 +24,10 @@ class AiohttpDeadlineExceededError(DeadlineExceededError, aiohttp.ServerTimeoutE
"""Total deadline exhausted, catchable as asyncio.TimeoutError and aiohttp.ClientError."""


class AiohttpAttemptTimeoutError(AttemptTimeoutError, aiohttp.ServerTimeoutError):
"""Attempt ceiling exhausted, catchable as asyncio.TimeoutError and aiohttp.ClientError."""


class AiohttpTooManyRedirectsError(TooManyRedirectsError, aiohttp.TooManyRedirects):
"""Owned redirect limit exceeded, catchable as aiohttp.TooManyRedirects.

Expand All @@ -44,12 +54,15 @@ def translate_call_error(error: CallError) -> BaseException:
return AiohttpCircuitOpenError(error.key, error.retry_after)
if isinstance(error, DeadlineExceededError):
return AiohttpDeadlineExceededError(error.total)
if isinstance(error, AttemptTimeoutError):
return AiohttpAttemptTimeoutError(error.attempt)
if isinstance(error, TooManyRedirectsError):
return AiohttpTooManyRedirectsError(error.hops)
return error


__all__ = [
"AiohttpAttemptTimeoutError",
"AiohttpCircuitOpenError",
"AiohttpDeadlineExceededError",
"AiohttpTooManyRedirectsError",
Expand Down
3 changes: 3 additions & 0 deletions clientwright/adapters/httpx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
if TYPE_CHECKING:
from .adapter import HttpxAdapter as HttpxAdapter
from .capabilities import CAPABILITIES as CAPABILITIES
from .errors import HttpxAttemptTimeoutError as HttpxAttemptTimeoutError
from .errors import HttpxCircuitOpenError as HttpxCircuitOpenError
from .errors import HttpxDeadlineExceededError as HttpxDeadlineExceededError
from .errors import HttpxTooManyRedirectsError as HttpxTooManyRedirectsError
Expand All @@ -22,6 +23,7 @@
_EXPORTS = {
"CAPABILITIES": "capabilities",
"HttpxAdapter": "adapter",
"HttpxAttemptTimeoutError": "errors",
"HttpxCircuitOpenError": "errors",
"HttpxDeadlineExceededError": "errors",
"HttpxTooManyRedirectsError": "errors",
Expand All @@ -39,6 +41,7 @@ def __getattr__(name: str) -> Any:
"IDEMPOTENT_EXTENSION",
"ROUTE_EXTENSION",
"HttpxAdapter",
"HttpxAttemptTimeoutError",
"HttpxCircuitOpenError",
"HttpxDeadlineExceededError",
"HttpxTooManyRedirectsError",
Expand Down
9 changes: 7 additions & 2 deletions clientwright/adapters/httpx/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from __future__ import annotations

from ...core.errors import CircuitOpenError, DeadlineExceededError, TooManyRedirectsError
from ...core.errors import AttemptTimeoutError, CircuitOpenError, DeadlineExceededError, TooManyRedirectsError
from .._httpx_shared import make_error_translator
from ._imports import httpx

Expand All @@ -19,15 +19,20 @@ class HttpxDeadlineExceededError(DeadlineExceededError, httpx.TimeoutException):
"""Total deadline exhausted, catchable as httpx.TimeoutException."""


class HttpxAttemptTimeoutError(AttemptTimeoutError, httpx.TimeoutException):
"""Attempt ceiling exhausted, catchable as httpx.TimeoutException."""


class HttpxTooManyRedirectsError(TooManyRedirectsError, httpx.TooManyRedirects):
"""Owned redirect limit exceeded, catchable as httpx.TooManyRedirects."""


translate_call_error = make_error_translator(
HttpxCircuitOpenError, HttpxDeadlineExceededError, HttpxTooManyRedirectsError
HttpxCircuitOpenError, HttpxDeadlineExceededError, HttpxTooManyRedirectsError, HttpxAttemptTimeoutError
)

__all__ = [
"HttpxAttemptTimeoutError",
"HttpxCircuitOpenError",
"HttpxDeadlineExceededError",
"HttpxTooManyRedirectsError",
Expand Down
3 changes: 3 additions & 0 deletions clientwright/adapters/httpx2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
if TYPE_CHECKING:
from .adapter import HttpxAdapter as HttpxAdapter
from .capabilities import CAPABILITIES as CAPABILITIES
from .errors import HttpxAttemptTimeoutError as HttpxAttemptTimeoutError
from .errors import HttpxCircuitOpenError as HttpxCircuitOpenError
from .errors import HttpxDeadlineExceededError as HttpxDeadlineExceededError
from .errors import HttpxTooManyRedirectsError as HttpxTooManyRedirectsError
Expand All @@ -23,6 +24,7 @@
_EXPORTS = {
"CAPABILITIES": "capabilities",
"HttpxAdapter": "adapter",
"HttpxAttemptTimeoutError": "errors",
"HttpxCircuitOpenError": "errors",
"HttpxDeadlineExceededError": "errors",
"HttpxTooManyRedirectsError": "errors",
Expand All @@ -40,6 +42,7 @@ def __getattr__(name: str) -> Any:
"IDEMPOTENT_EXTENSION",
"ROUTE_EXTENSION",
"HttpxAdapter",
"HttpxAttemptTimeoutError",
"HttpxCircuitOpenError",
"HttpxDeadlineExceededError",
"HttpxTooManyRedirectsError",
Expand Down
9 changes: 7 additions & 2 deletions clientwright/adapters/httpx2/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from __future__ import annotations

from ...core.errors import CircuitOpenError, DeadlineExceededError, TooManyRedirectsError
from ...core.errors import AttemptTimeoutError, CircuitOpenError, DeadlineExceededError, TooManyRedirectsError
from .._httpx_shared import make_error_translator
from ._imports import httpx2

Expand All @@ -20,15 +20,20 @@ class HttpxDeadlineExceededError(DeadlineExceededError, httpx2.TimeoutException)
"""Total deadline exhausted, catchable as httpx2.TimeoutException."""


class HttpxAttemptTimeoutError(AttemptTimeoutError, httpx2.TimeoutException):
"""Attempt ceiling exhausted, catchable as httpx2.TimeoutException."""


class HttpxTooManyRedirectsError(TooManyRedirectsError, httpx2.TooManyRedirects):
"""Owned redirect limit exceeded, catchable as httpx2.TooManyRedirects."""


translate_call_error = make_error_translator(
HttpxCircuitOpenError, HttpxDeadlineExceededError, HttpxTooManyRedirectsError
HttpxCircuitOpenError, HttpxDeadlineExceededError, HttpxTooManyRedirectsError, HttpxAttemptTimeoutError
)

__all__ = [
"HttpxAttemptTimeoutError",
"HttpxCircuitOpenError",
"HttpxDeadlineExceededError",
"HttpxTooManyRedirectsError",
Expand Down
5 changes: 5 additions & 0 deletions clientwright/adapters/requests/capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,17 @@
}
),
collapses={
FailureKind.ATTEMPT_TIMEOUT: FailureKind.READ_TIMEOUT,
FailureKind.POOL_TIMEOUT: FailureKind.CONNECT_TIMEOUT,
FailureKind.PROTOCOL_ERROR: FailureKind.DISCONNECTED,
FailureKind.WRITE_TIMEOUT: FailureKind.TOTAL_TIMEOUT,
},
notes={
"sync_only": "requests has no async client; build_async raises.",
"attempt_timeout": (
"A sync runtime cannot cancel an attempt, so no ceiling ever fires; the stall it would have caught "
"arrives as the clamped read phase."
),
"no_session_timeout": (
"requests has NO session-level timeout default - a bare session.get() hangs forever. The engine "
"closes that hole: every attempt is sent with the planned (connect, read) tuple."
Expand Down
5 changes: 5 additions & 0 deletions clientwright/adapters/urllib3/capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,16 @@
}
),
collapses={
FailureKind.ATTEMPT_TIMEOUT: FailureKind.READ_TIMEOUT,
FailureKind.PROTOCOL_ERROR: FailureKind.DISCONNECTED,
FailureKind.WRITE_TIMEOUT: FailureKind.TOTAL_TIMEOUT,
},
notes={
"sync_only": "urllib3 has no async client; build_async raises.",
"attempt_timeout": (
"A sync runtime cannot cancel an attempt, so no ceiling ever fires; the stall it would have caught "
"arrives as the clamped read phase."
),
"seam": (
"The engine is injected as an INSTANCE urlopen on a genuine PoolManager (type(client) is "
"urllib3.PoolManager); recursive native redirect hops re-enter it and pass straight through."
Expand Down
2 changes: 2 additions & 0 deletions clientwright/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class RetryMode(StrEnum):
FailureKind.DNS_ERROR,
FailureKind.POOL_TIMEOUT,
FailureKind.READ_TIMEOUT,
FailureKind.ATTEMPT_TIMEOUT,
FailureKind.DISCONNECTED,
}
)
Expand All @@ -99,6 +100,7 @@ class RetryMode(StrEnum):
FailureKind.READ_TIMEOUT,
FailureKind.WRITE_TIMEOUT,
FailureKind.POOL_TIMEOUT,
FailureKind.ATTEMPT_TIMEOUT,
FailureKind.TOTAL_TIMEOUT,
FailureKind.CONNECT_ERROR,
FailureKind.DNS_ERROR,
Expand Down
Loading