kash-shell 0.3.22__py3-none-any.whl → 0.3.24__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kash/actions/core/combine_docs.py +52 -0
- kash/actions/core/concat_docs.py +47 -0
- kash/commands/workspace/workspace_commands.py +2 -2
- kash/config/logger.py +3 -2
- kash/config/settings.py +8 -0
- kash/docs/markdown/topics/a2_installation.md +2 -2
- kash/embeddings/embeddings.py +1 -1
- kash/exec/action_exec.py +1 -1
- kash/exec/fetch_url_items.py +52 -16
- kash/file_storage/file_store.py +3 -3
- kash/llm_utils/llm_completion.py +1 -1
- kash/mcp/mcp_cli.py +2 -2
- kash/utils/api_utils/api_retries.py +348 -14
- kash/utils/api_utils/gather_limited.py +366 -512
- kash/utils/api_utils/http_utils.py +46 -0
- kash/utils/api_utils/progress_protocol.py +49 -56
- kash/utils/rich_custom/multitask_status.py +70 -21
- kash/utils/text_handling/markdown_utils.py +14 -3
- kash/web_content/web_extract.py +13 -9
- kash/web_content/web_fetch.py +289 -60
- kash/web_content/web_page_model.py +5 -0
- {kash_shell-0.3.22.dist-info → kash_shell-0.3.24.dist-info}/METADATA +5 -3
- {kash_shell-0.3.22.dist-info → kash_shell-0.3.24.dist-info}/RECORD +26 -23
- {kash_shell-0.3.22.dist-info → kash_shell-0.3.24.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.22.dist-info → kash_shell-0.3.24.dist-info}/entry_points.txt +0 -0
- {kash_shell-0.3.22.dist-info → kash_shell-0.3.24.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,6 +3,43 @@ from __future__ import annotations
|
|
|
3
3
|
import random
|
|
4
4
|
from collections.abc import Callable
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
from kash.utils.api_utils.http_utils import extract_http_status_code
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HTTPRetryBehavior(Enum):
|
|
12
|
+
"""HTTP status code retry behavior classification."""
|
|
13
|
+
|
|
14
|
+
FULL = "full"
|
|
15
|
+
"""Fully retry these status codes (e.g., 429, 500, 502, 503, 504)"""
|
|
16
|
+
|
|
17
|
+
CONSERVATIVE = "conservative"
|
|
18
|
+
"""Retry conservatively: may indicate rate limiting or temporary issues (e.g., 403, 408)"""
|
|
19
|
+
|
|
20
|
+
NEVER = "never"
|
|
21
|
+
"""Never retry these status codes (e.g., 400, 401, 404, 410)"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Default HTTP status code retry classifications
|
|
25
|
+
DEFAULT_HTTP_RETRY_MAP: dict[int, HTTPRetryBehavior] = {
|
|
26
|
+
# Fully retriable: server errors and explicit rate limiting
|
|
27
|
+
429: HTTPRetryBehavior.FULL, # Too Many Requests
|
|
28
|
+
500: HTTPRetryBehavior.FULL, # Internal Server Error
|
|
29
|
+
502: HTTPRetryBehavior.FULL, # Bad Gateway
|
|
30
|
+
503: HTTPRetryBehavior.FULL, # Service Unavailable
|
|
31
|
+
504: HTTPRetryBehavior.FULL, # Gateway Timeout
|
|
32
|
+
# Conservatively retriable: might be temporary
|
|
33
|
+
403: HTTPRetryBehavior.CONSERVATIVE, # Forbidden (could be rate limiting)
|
|
34
|
+
408: HTTPRetryBehavior.CONSERVATIVE, # Request Timeout
|
|
35
|
+
# Never retriable: client errors
|
|
36
|
+
400: HTTPRetryBehavior.NEVER, # Bad Request
|
|
37
|
+
401: HTTPRetryBehavior.NEVER, # Unauthorized
|
|
38
|
+
404: HTTPRetryBehavior.NEVER, # Not Found
|
|
39
|
+
405: HTTPRetryBehavior.NEVER, # Method Not Allowed
|
|
40
|
+
410: HTTPRetryBehavior.NEVER, # Gone
|
|
41
|
+
422: HTTPRetryBehavior.NEVER, # Unprocessable Entity
|
|
42
|
+
}
|
|
6
43
|
|
|
7
44
|
|
|
8
45
|
class RetryException(RuntimeError):
|
|
@@ -29,7 +66,7 @@ class RetryExhaustedException(RetryException):
|
|
|
29
66
|
|
|
30
67
|
def default_is_retriable(exception: Exception) -> bool:
|
|
31
68
|
"""
|
|
32
|
-
Default retriable exception checker
|
|
69
|
+
Default retriable exception checker with HTTP status code awareness.
|
|
33
70
|
|
|
34
71
|
Args:
|
|
35
72
|
exception: The exception to check
|
|
@@ -51,12 +88,22 @@ def default_is_retriable(exception: Exception) -> bool:
|
|
|
51
88
|
):
|
|
52
89
|
return True
|
|
53
90
|
except ImportError:
|
|
54
|
-
# LiteLLM not available, fall back to
|
|
91
|
+
# LiteLLM not available, fall back to other detection methods
|
|
55
92
|
pass
|
|
56
93
|
|
|
57
|
-
#
|
|
94
|
+
# Try to extract HTTP status code for more precise handling
|
|
95
|
+
status_code = extract_http_status_code(exception)
|
|
96
|
+
if status_code is not None:
|
|
97
|
+
return is_http_status_retriable(status_code, DEFAULT_HTTP_RETRY_MAP)
|
|
98
|
+
|
|
99
|
+
# Fallback to string-based detection for transient errors
|
|
58
100
|
exception_str = str(exception).lower()
|
|
59
|
-
|
|
101
|
+
|
|
102
|
+
# Check exception type names for common transient network errors
|
|
103
|
+
exception_type = type(exception).__name__.lower()
|
|
104
|
+
|
|
105
|
+
transient_error_indicators = [
|
|
106
|
+
# Rate limiting and quota errors
|
|
60
107
|
"rate limit",
|
|
61
108
|
"too many requests",
|
|
62
109
|
"try again later",
|
|
@@ -65,9 +112,92 @@ def default_is_retriable(exception: Exception) -> bool:
|
|
|
65
112
|
"throttled",
|
|
66
113
|
"rate_limit_error",
|
|
67
114
|
"ratelimiterror",
|
|
115
|
+
# Server errors
|
|
116
|
+
"server error",
|
|
117
|
+
"service unavailable",
|
|
118
|
+
"bad gateway",
|
|
119
|
+
"gateway timeout",
|
|
120
|
+
"internal server error",
|
|
121
|
+
"502",
|
|
122
|
+
"503",
|
|
123
|
+
"504",
|
|
124
|
+
"500",
|
|
125
|
+
# Network connectivity errors
|
|
126
|
+
"connection timeout",
|
|
127
|
+
"connection timed out",
|
|
128
|
+
"read timeout",
|
|
129
|
+
"timeout error",
|
|
130
|
+
"timed out",
|
|
131
|
+
"connection reset",
|
|
132
|
+
"connection refused",
|
|
133
|
+
"connection aborted",
|
|
134
|
+
"connection error",
|
|
135
|
+
"network error",
|
|
136
|
+
"network unreachable",
|
|
137
|
+
"network is unreachable",
|
|
138
|
+
"no route to host",
|
|
139
|
+
"temporary failure",
|
|
140
|
+
"name resolution failed",
|
|
141
|
+
"dns",
|
|
142
|
+
"resolver",
|
|
143
|
+
# SSL/TLS transient errors
|
|
144
|
+
"ssl error",
|
|
145
|
+
"certificate verify failed",
|
|
146
|
+
"handshake timeout",
|
|
147
|
+
# Common transient exception types
|
|
148
|
+
"connectionerror",
|
|
149
|
+
"timeouterror",
|
|
150
|
+
"connecttimeout",
|
|
151
|
+
"readtimeout",
|
|
152
|
+
"httperror",
|
|
153
|
+
"requestexception",
|
|
68
154
|
]
|
|
69
155
|
|
|
70
|
-
|
|
156
|
+
# Check both exception message and type name
|
|
157
|
+
return any(indicator in exception_str for indicator in transient_error_indicators) or any(
|
|
158
|
+
indicator in exception_type for indicator in transient_error_indicators
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def is_http_status_retriable(
|
|
163
|
+
status_code: int,
|
|
164
|
+
retry_policy: dict[int, HTTPRetryBehavior] | None = None,
|
|
165
|
+
) -> bool:
|
|
166
|
+
"""
|
|
167
|
+
Determine if an HTTP status code should be retried.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
status_code: HTTP status code
|
|
171
|
+
retry_policy: Custom retry behavior policy (uses default if None)
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
True if the status code should be retried
|
|
175
|
+
"""
|
|
176
|
+
if retry_policy is None:
|
|
177
|
+
retry_policy = DEFAULT_HTTP_RETRY_MAP
|
|
178
|
+
|
|
179
|
+
behavior = retry_policy.get(status_code)
|
|
180
|
+
|
|
181
|
+
if behavior == HTTPRetryBehavior.FULL:
|
|
182
|
+
return True
|
|
183
|
+
elif behavior == HTTPRetryBehavior.CONSERVATIVE:
|
|
184
|
+
return True # Conservative retries are enabled by default
|
|
185
|
+
elif behavior == HTTPRetryBehavior.NEVER:
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
# Unknown status code: use heuristics
|
|
189
|
+
if 500 <= status_code <= 599:
|
|
190
|
+
# Server errors are generally retriable
|
|
191
|
+
return True
|
|
192
|
+
elif status_code == 429:
|
|
193
|
+
# Rate limiting is always retriable
|
|
194
|
+
return True
|
|
195
|
+
elif 400 <= status_code <= 499:
|
|
196
|
+
# Client errors are generally not retriable, except for specific cases
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
# Default to not retriable for unknown codes
|
|
200
|
+
return False
|
|
71
201
|
|
|
72
202
|
|
|
73
203
|
@dataclass(frozen=True)
|
|
@@ -92,19 +222,74 @@ class RetrySettings:
|
|
|
92
222
|
"""Exponential backoff multiplier"""
|
|
93
223
|
|
|
94
224
|
is_retriable: Callable[[Exception], bool] = default_is_retriable
|
|
95
|
-
"""Function to determine if
|
|
225
|
+
"""Function to determine if non-HTTP exceptions should be retried (network errors, timeouts, etc.)"""
|
|
226
|
+
|
|
227
|
+
http_retry_policy: dict[int, HTTPRetryBehavior] | None = None
|
|
228
|
+
"""Custom HTTP status code retry behavior policy (None = use defaults)"""
|
|
229
|
+
|
|
230
|
+
def should_retry(self, exception: Exception) -> bool:
|
|
231
|
+
"""
|
|
232
|
+
Determine if an exception should be retried.
|
|
233
|
+
|
|
234
|
+
First checks for HTTP status codes and uses http_retry_policy if present.
|
|
235
|
+
For non-HTTP exceptions, uses the is_retriable function to determine
|
|
236
|
+
if other exception types (network errors, timeouts, etc.) should be retried.
|
|
237
|
+
"""
|
|
238
|
+
# First check if this is an HTTP exception with a status code
|
|
239
|
+
status_code = extract_http_status_code(exception)
|
|
240
|
+
if status_code:
|
|
241
|
+
retry_policy = (
|
|
242
|
+
self.http_retry_policy
|
|
243
|
+
if self.http_retry_policy is not None
|
|
244
|
+
else DEFAULT_HTTP_RETRY_MAP
|
|
245
|
+
)
|
|
246
|
+
return is_http_status_retriable(status_code, retry_policy)
|
|
247
|
+
|
|
248
|
+
# Not an HTTP error - use is_retriable for other exception types
|
|
249
|
+
# (network errors, timeouts, connection issues, etc.)
|
|
250
|
+
return self.is_retriable(exception)
|
|
96
251
|
|
|
97
252
|
|
|
98
253
|
DEFAULT_RETRIES = RetrySettings(
|
|
99
|
-
max_task_retries=
|
|
100
|
-
max_total_retries=
|
|
254
|
+
max_task_retries=15,
|
|
255
|
+
max_total_retries=1000,
|
|
101
256
|
initial_backoff=1.0,
|
|
102
|
-
max_backoff=
|
|
103
|
-
backoff_factor=
|
|
257
|
+
max_backoff=60.0,
|
|
258
|
+
backoff_factor=1.5,
|
|
104
259
|
is_retriable=default_is_retriable,
|
|
105
260
|
)
|
|
106
261
|
"""Reasonable default retry settings with both per-task and global limits."""
|
|
107
262
|
|
|
263
|
+
# Conservative retry settings use a custom retry policy that excludes conservative retries
|
|
264
|
+
_CONSERVATIVE_HTTP_RETRY_POLICY = {
|
|
265
|
+
# Fully retriable: server errors and explicit rate limiting
|
|
266
|
+
429: HTTPRetryBehavior.FULL,
|
|
267
|
+
500: HTTPRetryBehavior.FULL,
|
|
268
|
+
502: HTTPRetryBehavior.FULL,
|
|
269
|
+
503: HTTPRetryBehavior.FULL,
|
|
270
|
+
504: HTTPRetryBehavior.FULL,
|
|
271
|
+
# Conservative codes become NEVER for conservative mode
|
|
272
|
+
403: HTTPRetryBehavior.NEVER,
|
|
273
|
+
408: HTTPRetryBehavior.NEVER,
|
|
274
|
+
# Never retriable: client errors
|
|
275
|
+
400: HTTPRetryBehavior.NEVER,
|
|
276
|
+
401: HTTPRetryBehavior.NEVER,
|
|
277
|
+
404: HTTPRetryBehavior.NEVER,
|
|
278
|
+
405: HTTPRetryBehavior.NEVER,
|
|
279
|
+
410: HTTPRetryBehavior.NEVER,
|
|
280
|
+
422: HTTPRetryBehavior.NEVER,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
CONSERVATIVE_RETRIES = RetrySettings(
|
|
284
|
+
max_task_retries=5,
|
|
285
|
+
max_total_retries=50,
|
|
286
|
+
initial_backoff=2.0,
|
|
287
|
+
max_backoff=60.0,
|
|
288
|
+
backoff_factor=2.5,
|
|
289
|
+
http_retry_policy=_CONSERVATIVE_HTTP_RETRY_POLICY,
|
|
290
|
+
)
|
|
291
|
+
"""Conservative retry settings - fewer retries, longer backoff, no conservative HTTP retries."""
|
|
292
|
+
|
|
108
293
|
|
|
109
294
|
NO_RETRIES = RetrySettings(
|
|
110
295
|
max_task_retries=0,
|
|
@@ -190,9 +375,97 @@ def calculate_backoff(
|
|
|
190
375
|
## Tests
|
|
191
376
|
|
|
192
377
|
|
|
378
|
+
def test_extract_http_status_code():
|
|
379
|
+
"""Test HTTP status code extraction from various exception types."""
|
|
380
|
+
|
|
381
|
+
class MockHTTPXResponse:
|
|
382
|
+
def __init__(self, status_code):
|
|
383
|
+
self.status_code = status_code
|
|
384
|
+
|
|
385
|
+
class MockHTTPXException(Exception):
|
|
386
|
+
def __init__(self, status_code):
|
|
387
|
+
self.response = MockHTTPXResponse(status_code)
|
|
388
|
+
super().__init__(f"HTTP {status_code} error")
|
|
389
|
+
|
|
390
|
+
class MockAioHTTPException(Exception):
|
|
391
|
+
def __init__(self, status):
|
|
392
|
+
self.status = status
|
|
393
|
+
super().__init__(f"HTTP {status} error")
|
|
394
|
+
|
|
395
|
+
# Test httpx-style exceptions
|
|
396
|
+
assert extract_http_status_code(MockHTTPXException(403)) == 403
|
|
397
|
+
assert extract_http_status_code(MockHTTPXException(429)) == 429
|
|
398
|
+
|
|
399
|
+
# Test aiohttp-style exceptions
|
|
400
|
+
assert extract_http_status_code(MockAioHTTPException(500)) == 500
|
|
401
|
+
|
|
402
|
+
# Test string parsing fallback
|
|
403
|
+
assert extract_http_status_code(Exception("Client error '403 Forbidden'")) == 403
|
|
404
|
+
assert extract_http_status_code(Exception("HTTP 429 Too Many Requests")) == 429
|
|
405
|
+
assert extract_http_status_code(Exception("500 error occurred")) == 500
|
|
406
|
+
|
|
407
|
+
# Test no status code
|
|
408
|
+
assert extract_http_status_code(Exception("Network error")) is None
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def test_is_http_status_retriable():
|
|
412
|
+
"""Test HTTP status code retry logic."""
|
|
413
|
+
|
|
414
|
+
# Fully retriable
|
|
415
|
+
assert is_http_status_retriable(429) # Too Many Requests
|
|
416
|
+
assert is_http_status_retriable(500) # Internal Server Error
|
|
417
|
+
assert is_http_status_retriable(502) # Bad Gateway
|
|
418
|
+
assert is_http_status_retriable(503) # Service Unavailable
|
|
419
|
+
assert is_http_status_retriable(504) # Gateway Timeout
|
|
420
|
+
|
|
421
|
+
# Conservative retriable (enabled by default)
|
|
422
|
+
assert is_http_status_retriable(403) # Forbidden
|
|
423
|
+
assert is_http_status_retriable(408) # Request Timeout
|
|
424
|
+
|
|
425
|
+
# Conservative retriable with custom conservative policy (disabled)
|
|
426
|
+
assert not is_http_status_retriable(403, _CONSERVATIVE_HTTP_RETRY_POLICY)
|
|
427
|
+
assert not is_http_status_retriable(408, _CONSERVATIVE_HTTP_RETRY_POLICY)
|
|
428
|
+
|
|
429
|
+
# Never retriable
|
|
430
|
+
assert not is_http_status_retriable(400) # Bad Request
|
|
431
|
+
assert not is_http_status_retriable(401) # Unauthorized
|
|
432
|
+
assert not is_http_status_retriable(404) # Not Found
|
|
433
|
+
assert not is_http_status_retriable(410) # Gone
|
|
434
|
+
|
|
435
|
+
# Unknown status codes - use heuristics
|
|
436
|
+
assert is_http_status_retriable(599) # Unknown 5xx - retriable
|
|
437
|
+
assert not is_http_status_retriable(499) # Unknown 4xx - not retriable
|
|
438
|
+
assert not is_http_status_retriable(299) # Unknown 2xx - not retriable
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def test_default_is_retriable_with_http():
|
|
442
|
+
"""Test enhanced default_is_retriable with HTTP status code awareness."""
|
|
443
|
+
|
|
444
|
+
class MockHTTPXResponse:
|
|
445
|
+
def __init__(self, status_code):
|
|
446
|
+
self.status_code = status_code
|
|
447
|
+
|
|
448
|
+
class MockHTTPXException(Exception):
|
|
449
|
+
def __init__(self, status_code):
|
|
450
|
+
self.response = MockHTTPXResponse(status_code)
|
|
451
|
+
super().__init__(f"HTTP {status_code} error")
|
|
452
|
+
|
|
453
|
+
# Test HTTP exceptions with known status codes
|
|
454
|
+
assert default_is_retriable(MockHTTPXException(429)) # Rate limit - retriable
|
|
455
|
+
assert default_is_retriable(MockHTTPXException(500)) # Server error - retriable
|
|
456
|
+
assert default_is_retriable(MockHTTPXException(403)) # Conditional - retriable by default
|
|
457
|
+
assert not default_is_retriable(MockHTTPXException(404)) # Not found - not retriable
|
|
458
|
+
assert not default_is_retriable(MockHTTPXException(401)) # Unauthorized - not retriable
|
|
459
|
+
|
|
460
|
+
# Test string-based fallback still works
|
|
461
|
+
assert default_is_retriable(Exception("Rate limit exceeded"))
|
|
462
|
+
assert default_is_retriable(Exception("503 Service Unavailable"))
|
|
463
|
+
assert not default_is_retriable(Exception("Authentication failed"))
|
|
464
|
+
|
|
465
|
+
|
|
193
466
|
def test_default_is_retriable():
|
|
194
|
-
"""Test string-based
|
|
195
|
-
#
|
|
467
|
+
"""Test string-based transient error detection."""
|
|
468
|
+
# Rate limiting cases
|
|
196
469
|
assert default_is_retriable(Exception("Rate limit exceeded"))
|
|
197
470
|
assert default_is_retriable(Exception("Too many requests"))
|
|
198
471
|
assert default_is_retriable(Exception("HTTP 429 error"))
|
|
@@ -200,10 +473,30 @@ def test_default_is_retriable():
|
|
|
200
473
|
assert default_is_retriable(Exception("throttled"))
|
|
201
474
|
assert default_is_retriable(Exception("RateLimitError"))
|
|
202
475
|
|
|
203
|
-
#
|
|
476
|
+
# Network connectivity cases
|
|
477
|
+
assert default_is_retriable(Exception("Network error"))
|
|
478
|
+
assert default_is_retriable(Exception("Connection timeout"))
|
|
479
|
+
assert default_is_retriable(Exception("Connection timed out"))
|
|
480
|
+
assert default_is_retriable(Exception("Connection refused"))
|
|
481
|
+
assert default_is_retriable(Exception("Network unreachable"))
|
|
482
|
+
assert default_is_retriable(Exception("DNS resolution failed"))
|
|
483
|
+
assert default_is_retriable(Exception("SSL error"))
|
|
484
|
+
|
|
485
|
+
# Exception type-based detection
|
|
486
|
+
class ConnectionError(Exception):
|
|
487
|
+
pass
|
|
488
|
+
|
|
489
|
+
class TimeoutError(Exception):
|
|
490
|
+
pass
|
|
491
|
+
|
|
492
|
+
assert default_is_retriable(ConnectionError("Some connection issue"))
|
|
493
|
+
assert default_is_retriable(TimeoutError("Operation timed out"))
|
|
494
|
+
|
|
495
|
+
# Non-retriable cases
|
|
204
496
|
assert not default_is_retriable(Exception("Authentication failed"))
|
|
205
497
|
assert not default_is_retriable(Exception("Invalid API key"))
|
|
206
|
-
assert not default_is_retriable(Exception("
|
|
498
|
+
assert not default_is_retriable(Exception("Permission denied"))
|
|
499
|
+
assert not default_is_retriable(Exception("File not found"))
|
|
207
500
|
|
|
208
501
|
|
|
209
502
|
def test_default_is_retriable_litellm():
|
|
@@ -303,3 +596,44 @@ def test_calculate_backoff():
|
|
|
303
596
|
backoff_factor=2.0,
|
|
304
597
|
)
|
|
305
598
|
assert high_backoff <= 5.0
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def test_retry_settings_should_retry():
|
|
602
|
+
"""Test RetrySettings.should_retry method with custom HTTP maps."""
|
|
603
|
+
|
|
604
|
+
class MockHTTPXResponse:
|
|
605
|
+
def __init__(self, status_code):
|
|
606
|
+
self.status_code = status_code
|
|
607
|
+
|
|
608
|
+
class MockHTTPXException(Exception):
|
|
609
|
+
def __init__(self, status_code):
|
|
610
|
+
self.response = MockHTTPXResponse(status_code)
|
|
611
|
+
super().__init__(f"HTTP {status_code} error")
|
|
612
|
+
|
|
613
|
+
# Test with default settings (conservative retries enabled)
|
|
614
|
+
default_settings = RetrySettings(max_task_retries=3)
|
|
615
|
+
assert default_settings.should_retry(MockHTTPXException(429)) # Rate limit - retriable
|
|
616
|
+
assert default_settings.should_retry(MockHTTPXException(500)) # Server error - retriable
|
|
617
|
+
assert default_settings.should_retry(
|
|
618
|
+
MockHTTPXException(403)
|
|
619
|
+
) # Conservative - retriable by default
|
|
620
|
+
assert not default_settings.should_retry(MockHTTPXException(404)) # Not found - not retriable
|
|
621
|
+
|
|
622
|
+
# Test with conservative settings (conservative retries disabled)
|
|
623
|
+
conservative_settings = CONSERVATIVE_RETRIES
|
|
624
|
+
assert conservative_settings.should_retry(
|
|
625
|
+
MockHTTPXException(429)
|
|
626
|
+
) # Rate limit - still retriable
|
|
627
|
+
assert conservative_settings.should_retry(
|
|
628
|
+
MockHTTPXException(500)
|
|
629
|
+
) # Server error - still retriable
|
|
630
|
+
assert not conservative_settings.should_retry(
|
|
631
|
+
MockHTTPXException(403)
|
|
632
|
+
) # Conservative - now not retriable
|
|
633
|
+
assert not conservative_settings.should_retry(
|
|
634
|
+
MockHTTPXException(404)
|
|
635
|
+
) # Not found - still not retriable
|
|
636
|
+
|
|
637
|
+
# Test with non-HTTP exception
|
|
638
|
+
assert default_settings.should_retry(Exception("Network error"))
|
|
639
|
+
assert not default_settings.should_retry(Exception("Authentication failed"))
|