diff --git a/CHANGELOG.md b/CHANGELOG.md index 99ff954..451dae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to `uipath_llm_client` (core package) will be documented in this file. +## [1.13.0] - 2026-05-27 + +### Added +- `UiPathRequestTimeoutError` (HTTP 408) and `UiPathBadGatewayError` (HTTP 502) exception classes. Both are registered in `_STATUS_CODE_TO_EXCEPTION`, re-exported from `uipath.llm_client`, and inherit from `UiPathAPIError` for compatibility with existing handlers. + +### Changed +- **Default retry set expanded.** `_DEFAULT_RETRY_ON_EXCEPTIONS` in `uipath.llm_client.utils.retry` now covers `UiPathRequestTimeoutError` (408), `UiPathRateLimitError` (429), `UiPathBadGatewayError` (502), `UiPathServiceUnavailableError` (503), `UiPathGatewayTimeoutError` (504), and `UiPathTooManyRequestsError` (529) — up from `{429, 529}`. Applies to every provider client (`UiPathOpenAI`, `UiPathAnthropic*`, `UiPathGoogle`) since they all share the same `UiPathHttpxClient`-backed retry transport. `Retry-After` / `x-retry-after` headers and exponential backoff with jitter behave as before. +- **`UiPathHttpxClient` / `UiPathHttpxAsyncClient` default `max_retries` raised from `0` to `3`.** Callers that pass `max_retries=None` (or omit it entirely) now get 3 retries by default. Pass `max_retries=0` explicitly to opt out — `max_retries=0` continues to disable retries, so the opt-out path is unchanged. + ## [1.12.2] - 2026-05-24 ### Changed diff --git a/packages/uipath_langchain_client/CHANGELOG.md b/packages/uipath_langchain_client/CHANGELOG.md index 013c655..e0fade7 100644 --- a/packages/uipath_langchain_client/CHANGELOG.md +++ b/packages/uipath_langchain_client/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to `uipath_langchain_client` will be documented in this file. +## [1.13.0] - 2026-05-27 + +### Changed +- **`UiPathBaseLLMClient.max_retries` field default raised from `0` to `3`.** Every LangChain chat and embedding client built on this base (`UiPathChat`, `UiPathChatOpenAI`, `UiPathAzureChatOpenAI`, `UiPathChatAnthropic`, `UiPathChatAnthropicBedrock`, `UiPathChatBedrock`, `UiPathChatBedrockConverse`, `UiPathChatVertexAI`, `UiPathChatFireworks`, `UiPathChatLiteLLM`, plus the matching embeddings classes) now retries failed requests 3 times by default. Pass `max_retries=0` explicitly to disable retries — the opt-out path is unchanged. Combined with the expanded default retry set in `uipath-llm-client` 1.13.0, every LangChain client now retries on HTTP 408, 429, 502, 503, 504, and 529 out of the box. +- Bumped `uipath-llm-client` floor to `>=1.13.0` to pick up the expanded default retry set and the new `UiPathRequestTimeoutError` / `UiPathBadGatewayError` typed exceptions. + ## [1.12.2] - 2026-05-24 ### Changed diff --git a/packages/uipath_langchain_client/pyproject.toml b/packages/uipath_langchain_client/pyproject.toml index 7ea7365..feb4713 100644 --- a/packages/uipath_langchain_client/pyproject.toml +++ b/packages/uipath_langchain_client/pyproject.toml @@ -6,7 +6,7 @@ readme = "README.md" requires-python = ">=3.11" dependencies = [ "langchain>=1.2.15,<2.0.0", - "uipath-llm-client>=1.12.2,<2.0.0", + "uipath-llm-client>=1.13.0,<2.0.0", ] [project.optional-dependencies] diff --git a/packages/uipath_langchain_client/src/uipath_langchain_client/__version__.py b/packages/uipath_langchain_client/src/uipath_langchain_client/__version__.py index 6fa6f41..96b1a7b 100644 --- a/packages/uipath_langchain_client/src/uipath_langchain_client/__version__.py +++ b/packages/uipath_langchain_client/src/uipath_langchain_client/__version__.py @@ -1,3 +1,3 @@ __title__ = "UiPath LangChain Client" __description__ = "A Python client for interacting with UiPath's LLM services via LangChain." -__version__ = "1.12.2" +__version__ = "1.13.0" diff --git a/packages/uipath_langchain_client/src/uipath_langchain_client/base_client.py b/packages/uipath_langchain_client/src/uipath_langchain_client/base_client.py index df1e0e1..fccd696 100644 --- a/packages/uipath_langchain_client/src/uipath_langchain_client/base_client.py +++ b/packages/uipath_langchain_client/src/uipath_langchain_client/base_client.py @@ -145,8 +145,8 @@ class UiPathBaseLLMClient(BaseModel, ABC): description="Client-side request timeout in seconds", ) max_retries: int = Field( - default=0, - description="Maximum number of retries for failed requests", + default=3, + description="Maximum number of retries for failed requests. Pass 0 to disable retries.", ) retry_config: RetryConfig | None = Field( default=None, diff --git a/src/uipath/llm_client/__init__.py b/src/uipath/llm_client/__init__.py index c4e6261..da9f300 100644 --- a/src/uipath/llm_client/__init__.py +++ b/src/uipath/llm_client/__init__.py @@ -39,6 +39,7 @@ from uipath.llm_client.utils.exceptions import ( UiPathAPIError, UiPathAuthenticationError, + UiPathBadGatewayError, UiPathBadRequestError, UiPathConflictError, UiPathGatewayTimeoutError, @@ -46,6 +47,7 @@ UiPathNotFoundError, UiPathPermissionDeniedError, UiPathRateLimitError, + UiPathRequestTimeoutError, UiPathRequestTooLargeError, UiPathServiceUnavailableError, UiPathTooManyRequestsError, @@ -69,6 +71,7 @@ # Exceptions "UiPathAPIError", "UiPathAuthenticationError", + "UiPathBadGatewayError", "UiPathBadRequestError", "UiPathConflictError", "UiPathGatewayTimeoutError", @@ -76,6 +79,7 @@ "UiPathNotFoundError", "UiPathPermissionDeniedError", "UiPathRateLimitError", + "UiPathRequestTimeoutError", "UiPathRequestTooLargeError", "UiPathServiceUnavailableError", "UiPathTooManyRequestsError", diff --git a/src/uipath/llm_client/__version__.py b/src/uipath/llm_client/__version__.py index 8d889b7..b7fb238 100644 --- a/src/uipath/llm_client/__version__.py +++ b/src/uipath/llm_client/__version__.py @@ -1,3 +1,3 @@ __title__ = "UiPath LLM Client" __description__ = "A Python client for interacting with UiPath's LLM services." -__version__ = "1.12.2" +__version__ = "1.13.0" diff --git a/src/uipath/llm_client/httpx_client.py b/src/uipath/llm_client/httpx_client.py index 2969fe8..dc40ced 100644 --- a/src/uipath/llm_client/httpx_client.py +++ b/src/uipath/llm_client/httpx_client.py @@ -68,6 +68,10 @@ # Sentinel to distinguish "not provided" from an explicit ``None`` / ``False``. _UNSET: Any = object() +# Default applied when ``max_retries`` is left as ``None``. Callers can still +# opt out by passing ``max_retries=0`` explicitly. +_DEFAULT_MAX_RETRIES: typing.Final[int] = 3 + class UiPathHttpxClient(Client): """Synchronous HTTP client configured for UiPath LLM services. @@ -135,8 +139,8 @@ def __init__( captured_headers: Case-insensitive header name prefixes to capture from responses. Captured headers are stored in a ContextVar and can be retrieved with get_captured_response_headers(). Defaults to ("x-uipath-",). - max_retries: Maximum retry attempts for failed requests. Defaults to 0 - (retries disabled). Set to a positive integer to enable retries. + max_retries: Maximum retry attempts for failed requests. Defaults to 3 + when left as ``None``. Pass ``0`` to disable retries explicitly. retry_config: Custom retry configuration (backoff, retryable status codes). logger: Logger instance for request/response logging. auth: HTTP authentication (same as httpx.Client). Derived from @@ -193,7 +197,7 @@ def __init__( # Setup retry transport if not provided if transport is None: transport = RetryableHTTPTransport( - max_retries=max_retries if max_retries is not None else 0, + max_retries=max_retries if max_retries is not None else _DEFAULT_MAX_RETRIES, retry_config=retry_config, logger=logger, ) @@ -354,7 +358,7 @@ def __init__( # Setup retry transport if not provided if transport is None: transport = RetryableAsyncHTTPTransport( - max_retries=max_retries if max_retries is not None else 0, + max_retries=max_retries if max_retries is not None else _DEFAULT_MAX_RETRIES, retry_config=retry_config, logger=logger, ) diff --git a/src/uipath/llm_client/utils/exceptions.py b/src/uipath/llm_client/utils/exceptions.py index 3e13057..0ad93e3 100644 --- a/src/uipath/llm_client/utils/exceptions.py +++ b/src/uipath/llm_client/utils/exceptions.py @@ -118,6 +118,12 @@ class UiPathNotFoundError(UiPathAPIError): status_code: Literal[404] = 404 # pyright: ignore[reportIncompatibleVariableOverride] +class UiPathRequestTimeoutError(UiPathAPIError): + """HTTP 408 Request Timeout error.""" + + status_code: Literal[408] = 408 # pyright: ignore[reportIncompatibleVariableOverride] + + class UiPathConflictError(UiPathAPIError): """HTTP 409 Conflict error.""" @@ -211,6 +217,12 @@ class UiPathInternalServerError(UiPathAPIError): status_code: Literal[500] = 500 # pyright: ignore[reportIncompatibleVariableOverride] +class UiPathBadGatewayError(UiPathAPIError): + """HTTP 502 Bad Gateway error.""" + + status_code: Literal[502] = 502 # pyright: ignore[reportIncompatibleVariableOverride] + + class UiPathServiceUnavailableError(UiPathAPIError): """HTTP 503 Service Unavailable error.""" @@ -234,11 +246,13 @@ class UiPathTooManyRequestsError(UiPathAPIError): 401: UiPathAuthenticationError, 403: UiPathPermissionDeniedError, 404: UiPathNotFoundError, + 408: UiPathRequestTimeoutError, 409: UiPathConflictError, 413: UiPathRequestTooLargeError, 422: UiPathUnprocessableEntityError, 429: UiPathRateLimitError, 500: UiPathInternalServerError, + 502: UiPathBadGatewayError, 503: UiPathServiceUnavailableError, 504: UiPathGatewayTimeoutError, 529: UiPathTooManyRequestsError, @@ -266,11 +280,13 @@ def raise_for_status() -> Response: "UiPathAuthenticationError", "UiPathPermissionDeniedError", "UiPathNotFoundError", + "UiPathRequestTimeoutError", "UiPathConflictError", "UiPathRequestTooLargeError", "UiPathUnprocessableEntityError", "UiPathRateLimitError", "UiPathInternalServerError", + "UiPathBadGatewayError", "UiPathServiceUnavailableError", "UiPathGatewayTimeoutError", "UiPathTooManyRequestsError", diff --git a/src/uipath/llm_client/utils/retry.py b/src/uipath/llm_client/utils/retry.py index 32c89a3..d4ef183 100644 --- a/src/uipath/llm_client/utils/retry.py +++ b/src/uipath/llm_client/utils/retry.py @@ -49,13 +49,22 @@ from uipath.llm_client.utils.exceptions import ( UiPathAPIError, + UiPathBadGatewayError, + UiPathGatewayTimeoutError, UiPathRateLimitError, + UiPathRequestTimeoutError, + UiPathServiceUnavailableError, UiPathTooManyRequestsError, ) # Default retry configuration values +# Status codes retried by default: 408, 429, 502, 503, 504, 529. _DEFAULT_RETRY_ON_EXCEPTIONS: tuple[type[Exception], ...] = ( + UiPathRequestTimeoutError, UiPathRateLimitError, + UiPathBadGatewayError, + UiPathServiceUnavailableError, + UiPathGatewayTimeoutError, UiPathTooManyRequestsError, ) _DEFAULT_INITIAL_DELAY: float = 2.0 @@ -127,7 +136,7 @@ class RetryConfig(TypedDict): Attributes: retry_on_exceptions: Tuple of exception types to retry on. - Defaults to (UiPathRateLimitError,). + Defaults to the typed exceptions for HTTP 408, 429, 502, 503, 504, 529. initial_delay: Initial delay in seconds before first retry. Defaults to 2.0. max_delay: Maximum delay in seconds between retries. diff --git a/tests/core/features/test_exceptions.py b/tests/core/features/test_exceptions.py index 46810af..3f7b71a 100644 --- a/tests/core/features/test_exceptions.py +++ b/tests/core/features/test_exceptions.py @@ -9,12 +9,14 @@ from uipath.llm_client.utils.exceptions import ( UiPathAPIError, UiPathAuthenticationError, + UiPathBadGatewayError, UiPathBadRequestError, UiPathGatewayTimeoutError, UiPathInternalServerError, UiPathNotFoundError, UiPathPermissionDeniedError, UiPathRateLimitError, + UiPathRequestTimeoutError, UiPathServiceUnavailableError, UiPathTooManyRequestsError, patch_raise_for_status, @@ -170,8 +172,10 @@ def test_all_status_code_mappings(self): 401: UiPathAuthenticationError, 403: UiPathPermissionDeniedError, 404: UiPathNotFoundError, + 408: UiPathRequestTimeoutError, 429: UiPathRateLimitError, 500: UiPathInternalServerError, + 502: UiPathBadGatewayError, 503: UiPathServiceUnavailableError, 504: UiPathGatewayTimeoutError, 529: UiPathTooManyRequestsError, diff --git a/tests/core/features/test_httpx_client.py b/tests/core/features/test_httpx_client.py index 1d0c776..b19bd38 100644 --- a/tests/core/features/test_httpx_client.py +++ b/tests/core/features/test_httpx_client.py @@ -80,6 +80,24 @@ def test_client_with_retry_config(self): assert isinstance(client._transport, RetryableHTTPTransport) client.close() + def test_client_default_max_retries_is_three(self): + """Caller passing no ``max_retries`` should get the 3-retry default.""" + from uipath.llm_client.httpx_client import UiPathHttpxClient + + client = UiPathHttpxClient(base_url="https://example.com") + assert isinstance(client._transport, RetryableHTTPTransport) + assert client._transport.retryer is not None + client.close() + + def test_client_explicit_zero_disables_retries(self): + """Passing ``max_retries=0`` must still disable retries.""" + from uipath.llm_client.httpx_client import UiPathHttpxClient + + client = UiPathHttpxClient(base_url="https://example.com", max_retries=0) + assert isinstance(client._transport, RetryableHTTPTransport) + assert client._transport.retryer is None + client.close() + def test_client_with_byo_connection_id(self): """Test client adds BYO connection ID header.""" from uipath.llm_client.httpx_client import UiPathHttpxClient @@ -123,6 +141,22 @@ def test_async_client_with_retry_config(self): # Transport should be RetryableAsyncHTTPTransport assert isinstance(client._transport, RetryableAsyncHTTPTransport) + def test_async_client_default_max_retries_is_three(self): + """Async caller passing no ``max_retries`` should get the 3-retry default.""" + from uipath.llm_client.httpx_client import UiPathHttpxAsyncClient + + client = UiPathHttpxAsyncClient(base_url="https://example.com") + assert isinstance(client._transport, RetryableAsyncHTTPTransport) + assert client._transport.retryer is not None + + def test_async_client_explicit_zero_disables_retries(self): + """Async client: passing ``max_retries=0`` must still disable retries.""" + from uipath.llm_client.httpx_client import UiPathHttpxAsyncClient + + client = UiPathHttpxAsyncClient(base_url="https://example.com", max_retries=0) + assert isinstance(client._transport, RetryableAsyncHTTPTransport) + assert client._transport.retryer is None + class TestBuildRoutingHeaders: """Tests for build_routing_headers function.""" diff --git a/tests/core/features/test_retry.py b/tests/core/features/test_retry.py index 27f9a53..0a47d33 100644 --- a/tests/core/features/test_retry.py +++ b/tests/core/features/test_retry.py @@ -3,16 +3,45 @@ import logging from unittest.mock import MagicMock, patch +import httpx +import pytest + +from uipath.llm_client.httpx_client import UiPathHttpxAsyncClient, UiPathHttpxClient from uipath.llm_client.utils.exceptions import ( + UiPathBadGatewayError, + UiPathGatewayTimeoutError, UiPathInternalServerError, UiPathRateLimitError, + UiPathRequestTimeoutError, + UiPathServiceUnavailableError, + UiPathTooManyRequestsError, ) from uipath.llm_client.utils.retry import ( + _DEFAULT_RETRY_ON_EXCEPTIONS, RetryableAsyncHTTPTransport, RetryableHTTPTransport, RetryConfig, ) +_NO_DELAY_CONFIG: RetryConfig = {"initial_delay": 0, "max_delay": 0, "jitter": 0} + + +class TestDefaultRetryOnExceptions: + """Pins the default retry set so HTTP 408/429/502/503/504/529 stay retryable.""" + + def test_default_set_covers_required_status_codes(self): + assert set(_DEFAULT_RETRY_ON_EXCEPTIONS) == { + UiPathRequestTimeoutError, + UiPathRateLimitError, + UiPathBadGatewayError, + UiPathServiceUnavailableError, + UiPathGatewayTimeoutError, + UiPathTooManyRequestsError, + } + + def test_internal_server_error_is_not_retried_by_default(self): + assert UiPathInternalServerError not in _DEFAULT_RETRY_ON_EXCEPTIONS + class TestRetryConfig: """Tests for RetryConfig TypedDict.""" @@ -240,3 +269,79 @@ def test_with_logger_adds_before_sleep(self): result = _build_retryer(max_retries=2, retry_config=None, logger=logger) assert result is not None assert result.before_sleep is not None + + +class TestRetryOn504EndToEnd: + """End-to-end coverage that a 504 response actually triggers the retry loop. + + The other test classes verify the *configuration* (default exception set, + retryer wiring, wait math). These tests drive a real request through + ``RetryableHTTPTransport.handle_request`` / ``handle_async_request`` and + assert the underlying call fires ``max_retries`` times before the final 504 + response is returned. + """ + + def test_sync_504_retries_max_retries_times_then_returns_response(self): + calls = 0 + + def fake_504(self: httpx.HTTPTransport, request: httpx.Request) -> httpx.Response: + nonlocal calls + calls += 1 + return httpx.Response(504, content=b"{}", request=request) + + client = UiPathHttpxClient( + base_url="https://example.com", + max_retries=3, + retry_config=_NO_DELAY_CONFIG, + ) + try: + with patch.object(httpx.HTTPTransport, "handle_request", fake_504): + response = client.post("/anything", json={}) + finally: + client.close() + + assert calls == 3 + assert response.status_code == 504 + + def test_sync_max_retries_zero_makes_exactly_one_call(self): + calls = 0 + + def fake_504(self: httpx.HTTPTransport, request: httpx.Request) -> httpx.Response: + nonlocal calls + calls += 1 + return httpx.Response(504, content=b"{}", request=request) + + client = UiPathHttpxClient(base_url="https://example.com", max_retries=0) + try: + with patch.object(httpx.HTTPTransport, "handle_request", fake_504): + response = client.post("/anything", json={}) + finally: + client.close() + + assert calls == 1 + assert response.status_code == 504 + + @pytest.mark.asyncio + async def test_async_504_retries_max_retries_times_then_returns_response(self): + calls = 0 + + async def fake_504( + self: httpx.AsyncHTTPTransport, request: httpx.Request + ) -> httpx.Response: + nonlocal calls + calls += 1 + return httpx.Response(504, content=b"{}", request=request) + + client = UiPathHttpxAsyncClient( + base_url="https://example.com", + max_retries=3, + retry_config=_NO_DELAY_CONFIG, + ) + try: + with patch.object(httpx.AsyncHTTPTransport, "handle_async_request", fake_504): + response = await client.post("/anything", json={}) + finally: + await client.aclose() + + assert calls == 3 + assert response.status_code == 504 diff --git a/tests/core/smoke_test.py b/tests/core/smoke_test.py index a556dd4..314738e 100644 --- a/tests/core/smoke_test.py +++ b/tests/core/smoke_test.py @@ -62,6 +62,7 @@ def test_exception_imports(): from uipath.llm_client import ( UiPathAPIError, UiPathAuthenticationError, + UiPathBadGatewayError, UiPathBadRequestError, UiPathConflictError, UiPathGatewayTimeoutError, @@ -69,6 +70,7 @@ def test_exception_imports(): UiPathNotFoundError, UiPathPermissionDeniedError, UiPathRateLimitError, + UiPathRequestTimeoutError, UiPathRequestTooLargeError, UiPathServiceUnavailableError, UiPathTooManyRequestsError, @@ -79,6 +81,7 @@ def test_exception_imports(): exceptions = [ UiPathAPIError, UiPathAuthenticationError, + UiPathBadGatewayError, UiPathBadRequestError, UiPathConflictError, UiPathGatewayTimeoutError, @@ -86,6 +89,7 @@ def test_exception_imports(): UiPathNotFoundError, UiPathPermissionDeniedError, UiPathRateLimitError, + UiPathRequestTimeoutError, UiPathRequestTooLargeError, UiPathServiceUnavailableError, UiPathTooManyRequestsError, @@ -176,6 +180,7 @@ def test_exceptions_module_imports(): from uipath.llm_client.utils.exceptions import ( UiPathAPIError, UiPathAuthenticationError, + UiPathBadGatewayError, UiPathBadRequestError, UiPathConflictError, UiPathGatewayTimeoutError, @@ -183,6 +188,7 @@ def test_exceptions_module_imports(): UiPathNotFoundError, UiPathPermissionDeniedError, UiPathRateLimitError, + UiPathRequestTimeoutError, UiPathRequestTooLargeError, UiPathServiceUnavailableError, UiPathTooManyRequestsError, @@ -198,6 +204,7 @@ def test_exceptions_module_imports(): # Verify all specific exceptions inherit from UiPathAPIError specific_exceptions = [ UiPathAuthenticationError, + UiPathBadGatewayError, UiPathBadRequestError, UiPathConflictError, UiPathGatewayTimeoutError, @@ -205,6 +212,7 @@ def test_exceptions_module_imports(): UiPathNotFoundError, UiPathPermissionDeniedError, UiPathRateLimitError, + UiPathRequestTimeoutError, UiPathRequestTooLargeError, UiPathServiceUnavailableError, UiPathTooManyRequestsError, diff --git a/tests/langchain/features/test_default_max_retries.py b/tests/langchain/features/test_default_max_retries.py new file mode 100644 index 0000000..04fb4cb --- /dev/null +++ b/tests/langchain/features/test_default_max_retries.py @@ -0,0 +1,49 @@ +"""Tests pinning the default ``max_retries`` value on ``UiPathBaseLLMClient``. + +The default must stay at 3 so that every LangChain chat/embedding client built +on top of ``UiPathBaseLLMClient`` (OpenAI, Anthropic, Bedrock, Vertex AI, etc.) +retries transient HTTP failures by default. Callers can still opt out by +passing ``max_retries=0`` explicitly. +""" + +import os +from unittest.mock import patch + +from uipath_langchain_client.clients.normalized.chat_models import UiPathChat + +from uipath.llm_client.settings import LLMGatewaySettings +from uipath.llm_client.settings.utils import SingletonMeta +from uipath.llm_client.utils.retry import RetryableHTTPTransport + +LLMGW_ENV = { + "LLMGW_URL": "https://cloud.uipath.com", + "LLMGW_SEMANTIC_ORG_ID": "test-org-id", + "LLMGW_SEMANTIC_TENANT_ID": "test-tenant-id", + "LLMGW_REQUESTING_PRODUCT": "test-product", + "LLMGW_REQUESTING_FEATURE": "test-feature", + "LLMGW_ACCESS_TOKEN": "test-access-token", +} + + +class TestDefaultMaxRetries: + def setup_method(self): + SingletonMeta._instances.clear() + + def teardown_method(self): + SingletonMeta._instances.clear() + + def test_default_max_retries_is_three(self): + with patch.dict(os.environ, LLMGW_ENV, clear=True): + chat = UiPathChat(model="gpt-4o", settings=LLMGatewaySettings()) + assert chat.max_retries == 3 + transport = chat.uipath_sync_client._transport + assert isinstance(transport, RetryableHTTPTransport) + assert transport.retryer is not None + + def test_explicit_zero_disables_retries(self): + with patch.dict(os.environ, LLMGW_ENV, clear=True): + chat = UiPathChat(model="gpt-4o", settings=LLMGatewaySettings(), max_retries=0) + assert chat.max_retries == 0 + transport = chat.uipath_sync_client._transport + assert isinstance(transport, RetryableHTTPTransport) + assert transport.retryer is None