coding-client 1.21.2__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.
Files changed (109) hide show
  1. coding_client/__init__.py +1 -0
  2. coding_client/api/__init__.py +19 -0
  3. coding_client/api/exception_handlers.py +158 -0
  4. coding_client/api/logging_setup.py +44 -0
  5. coding_client/api/metrics.py +40 -0
  6. coding_client/api/openapi_config.py +124 -0
  7. coding_client/api/request_logging.py +60 -0
  8. coding_client/api/validation.py +150 -0
  9. coding_client/auth/__init__.py +78 -0
  10. coding_client/auth/_default_tenant.py +132 -0
  11. coding_client/auth/_header_trust.py +98 -0
  12. coding_client/auth/_onbehalf.py +315 -0
  13. coding_client/auth/_trusted_networks.py +94 -0
  14. coding_client/auth/dependencies.py +61 -0
  15. coding_client/auth/middleware.py +445 -0
  16. coding_client/auth/models.py +83 -0
  17. coding_client/auth/outbound.py +137 -0
  18. coding_client/auth/rbac.py +21 -0
  19. coding_client/auth/rbac_coding.py +5 -0
  20. coding_client/auth/tenant.py +650 -0
  21. coding_client/auth/websocket.py +148 -0
  22. coding_client/clients/__init__.py +24 -0
  23. coding_client/clients/base.py +131 -0
  24. coding_client/clients/coding_ai.py +33 -0
  25. coding_client/clients/coding_analysis.py +36 -0
  26. coding_client/clients/coding_containers.py +45 -0
  27. coding_client/clients/coding_github.py +127 -0
  28. coding_client/clients/coding_realtime.py +127 -0
  29. coding_client/clients/coding_security.py +51 -0
  30. coding_client/clients/coding_testing.py +31 -0
  31. coding_client/clients/coding_workspaces.py +265 -0
  32. coding_client/config/__init__.py +12 -0
  33. coding_client/config/base_settings.py +61 -0
  34. coding_client/config/yaml_config_loader.py +172 -0
  35. coding_client/db/__init__.py +42 -0
  36. coding_client/db/db_types.py +122 -0
  37. coding_client/db/exceptions.py +59 -0
  38. coding_client/db/postgres_connection.py +572 -0
  39. coding_client/db/tenant_query_helper.py +417 -0
  40. coding_client/db_init.py +49 -0
  41. coding_client/exceptions/__init__.py +67 -0
  42. coding_client/exceptions/api_exceptions.py +395 -0
  43. coding_client/exceptions/pydantic_ai_exceptions.py +533 -0
  44. coding_client/interfaces/__init__.py +22 -0
  45. coding_client/interfaces/cache_interface.py +179 -0
  46. coding_client/interfaces/container_interface.py +49 -0
  47. coding_client/interfaces/docker_interface.py +283 -0
  48. coding_client/interfaces/graph_interface.py +28 -0
  49. coding_client/interfaces/security_interface.py +72 -0
  50. coding_client/interfaces/webhook_interface.py +67 -0
  51. coding_client/llm/__init__.py +54 -0
  52. coding_client/llm/base_agent.py +195 -0
  53. coding_client/llm/context.py +121 -0
  54. coding_client/llm/responses.py +102 -0
  55. coding_client/llm/routing.py +82 -0
  56. coding_client/llm/settings.py +102 -0
  57. coding_client/models/__init__.py +62 -0
  58. coding_client/models/agent.py +38 -0
  59. coding_client/models/ai_test_failure.py +745 -0
  60. coding_client/models/analysis.py +330 -0
  61. coding_client/models/architecture.py +685 -0
  62. coding_client/models/bug.py +128 -0
  63. coding_client/models/code_graph.py +317 -0
  64. coding_client/models/container.py +241 -0
  65. coding_client/models/core.py +878 -0
  66. coding_client/models/docker_container.py +104 -0
  67. coding_client/models/github.py +293 -0
  68. coding_client/models/logging.py +29 -0
  69. coding_client/models/package_manager.py +257 -0
  70. coding_client/models/pr_validation.py +440 -0
  71. coding_client/models/pydantic_ai.py +395 -0
  72. coding_client/models/repo_scanner.py +249 -0
  73. coding_client/models/responses.py +431 -0
  74. coding_client/models/security.py +185 -0
  75. coding_client/models/static_analysis.py +508 -0
  76. coding_client/models/system.py +48 -0
  77. coding_client/models/test_executor.py +204 -0
  78. coding_client/models/testing.py +673 -0
  79. coding_client/models/testing_config.py +200 -0
  80. coding_client/models/validation.py +127 -0
  81. coding_client/models_init.py +272 -0
  82. coding_client/telemetry.py +195 -0
  83. coding_client/testing.py +180 -0
  84. coding_client/types/__init__.py +130 -0
  85. coding_client/types/constraint_types.py +276 -0
  86. coding_client/types/core_types.py +112 -0
  87. coding_client/types/state_machine.py +443 -0
  88. coding_client/utils/__init__.py +86 -0
  89. coding_client/utils/archive_utils.py +387 -0
  90. coding_client/utils/ast_debug.py +45 -0
  91. coding_client/utils/async_timeout.py +364 -0
  92. coding_client/utils/constants.py +518 -0
  93. coding_client/utils/content_processing.py +326 -0
  94. coding_client/utils/database.py +916 -0
  95. coding_client/utils/encoding_helper.py +222 -0
  96. coding_client/utils/enum_helpers.py +166 -0
  97. coding_client/utils/file_operations.py +177 -0
  98. coding_client/utils/filesystem_ops.py +528 -0
  99. coding_client/utils/http_client.py +562 -0
  100. coding_client/utils/path_utils.py +471 -0
  101. coding_client/utils/profiling.py +421 -0
  102. coding_client/utils/response_builder.py +240 -0
  103. coding_client/utils/retry_helper.py +712 -0
  104. coding_client/utils/security_validation.py +423 -0
  105. coding_client-1.21.2.dist-info/METADATA +234 -0
  106. coding_client-1.21.2.dist-info/RECORD +109 -0
  107. coding_client-1.21.2.dist-info/WHEEL +5 -0
  108. coding_client-1.21.2.dist-info/licenses/LICENSE +201 -0
  109. coding_client-1.21.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,562 @@
1
+ """Unified HTTP client wrapper with retry logic, error handling, and timeout management.
2
+
3
+ This module provides a standardized HTTP client that handles common patterns like
4
+ retries, timeouts, error handling, and logging across the application.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import inspect
11
+ import logging
12
+ import time
13
+ from functools import wraps
14
+ from typing import Any
15
+ from urllib.parse import urljoin
16
+
17
+ import httpx as requests # httpx as drop-in replacement for requests (CVE-2026-25645)
18
+
19
+ # Shims for requests API compatibility
20
+ HTTPAdapter = None
21
+ Retry = None
22
+
23
+ try:
24
+ import aiohttp # noqa: F401 — used by ``from aiohttp import`` below
25
+
26
+ HAS_AIOHTTP = True
27
+ except ImportError:
28
+ HAS_AIOHTTP = False
29
+
30
+ if HAS_AIOHTTP:
31
+ from aiohttp import ClientError, ClientSession, ClientTimeout
32
+ else:
33
+ ClientTimeout = None
34
+ ClientSession = None
35
+ ClientError = None
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ class HTTPClientError(Exception):
41
+ """Base exception for HTTP client errors."""
42
+
43
+ pass
44
+
45
+
46
+ class HTTPTimeoutError(HTTPClientError):
47
+ """Raised when an HTTP request times out."""
48
+
49
+ pass
50
+
51
+
52
+ class HTTPConnectionError(HTTPClientError):
53
+ """Raised when connection to server fails."""
54
+
55
+ pass
56
+
57
+
58
+ class HTTPResponseError(HTTPClientError):
59
+ """Raised when server returns an error response."""
60
+
61
+ def __init__(self, message: str, status_code: int, response_text: str = ""):
62
+ super().__init__(message)
63
+ self.status_code = status_code
64
+ self.response_text = response_text
65
+
66
+
67
+ class RetryConfig:
68
+ """Configuration for retry behavior."""
69
+
70
+ def __init__(
71
+ self,
72
+ total: int = 3,
73
+ backoff_factor: float = 0.3,
74
+ status_forcelist: list[int] | None = None,
75
+ method_whitelist: list[str] | None = None,
76
+ ):
77
+ self.total = total
78
+ self.backoff_factor = backoff_factor
79
+ self.status_forcelist = status_forcelist or [429, 500, 502, 503, 504]
80
+ self.method_whitelist = method_whitelist or [
81
+ "GET",
82
+ "PUT",
83
+ "DELETE",
84
+ "OPTIONS",
85
+ "TRACE",
86
+ "HEAD",
87
+ ]
88
+
89
+
90
+ class HTTPClient:
91
+ """Unified HTTP client with retry logic and error handling."""
92
+
93
+ DEFAULT_TIMEOUT = 30.0
94
+ DEFAULT_RETRY_CONFIG = RetryConfig()
95
+
96
+ def __init__(
97
+ self,
98
+ base_url: str | None = None,
99
+ timeout: float = DEFAULT_TIMEOUT,
100
+ retry_config: RetryConfig | None = None,
101
+ headers: dict[str, str] | None = None,
102
+ ):
103
+ """Initialize HTTP client.
104
+
105
+ Args:
106
+ base_url: Optional base URL for all requests
107
+ timeout: Default timeout in seconds
108
+ retry_config: Retry configuration
109
+ headers: Default headers to include in all requests
110
+ """
111
+ self.base_url = base_url
112
+ self.timeout = timeout
113
+ self.retry_config = retry_config or self.DEFAULT_RETRY_CONFIG
114
+ self.default_headers = headers or {}
115
+
116
+ # Setup sync session with retry adapter
117
+ self._sync_session = self._create_sync_session()
118
+
119
+ # Async session will be created on demand
120
+ self._async_session: ClientSession | None = None
121
+
122
+ def _create_sync_session(self) -> requests.Client:
123
+ """Create an httpx client with timeout configuration."""
124
+ transport = requests.HTTPTransport(retries=self.retry_config.total)
125
+ session = requests.Client(
126
+ transport=transport,
127
+ timeout=self.timeout,
128
+ )
129
+
130
+ # Set default headers
131
+ session.headers.update(self.default_headers)
132
+
133
+ return session
134
+
135
+ async def _ensure_async_session(self) -> ClientSession:
136
+ """Ensure async session is created."""
137
+ if not HAS_AIOHTTP:
138
+ raise ImportError("aiohttp module not available - install with 'pip install aiohttp'")
139
+
140
+ if self._async_session is None or self._async_session.closed:
141
+ assert ClientTimeout is not None, "aiohttp must be installed"
142
+ assert ClientSession is not None, "aiohttp must be installed"
143
+ timeout = ClientTimeout(total=self.timeout)
144
+ self._async_session = ClientSession(
145
+ timeout=timeout,
146
+ headers=self.default_headers,
147
+ )
148
+ return self._async_session
149
+
150
+ def _build_url(self, endpoint: str) -> str:
151
+ """Build full URL from endpoint."""
152
+ if self.base_url:
153
+ return urljoin(self.base_url, endpoint)
154
+ return endpoint
155
+
156
+ def _log_request(self, method: str, url: str, **kwargs):
157
+ """Log HTTP request details."""
158
+ logger.debug(
159
+ f"HTTP {method} {url}",
160
+ extra={
161
+ "http_method": method,
162
+ "http_url": url,
163
+ "http_params": kwargs.get("params"),
164
+ "http_headers": kwargs.get("headers"),
165
+ },
166
+ )
167
+
168
+ def _log_response(self, method: str, url: str, status_code: int, duration: float):
169
+ """Log HTTP response details."""
170
+ logger.debug(
171
+ f"HTTP {method} {url} - {status_code} ({duration:.2f}s)",
172
+ extra={
173
+ "http_method": method,
174
+ "http_url": url,
175
+ "http_status_code": status_code,
176
+ "http_duration": duration,
177
+ },
178
+ )
179
+
180
+ def _handle_response_error(self, response: requests.Response | Any, method: str, url: str):
181
+ """Handle HTTP error responses."""
182
+ status_code: int = (
183
+ response.status_code if hasattr(response, "status_code") else response.status
184
+ )
185
+
186
+ if 400 <= status_code < 500:
187
+ error_msg = f"Client error on {method} {url}: {status_code}"
188
+ response_text = ""
189
+ try:
190
+ if hasattr(response, "text"):
191
+ response_text = response.text
192
+ else:
193
+ # For async response, we'd need to await response.text()
194
+ # This is handled in the async methods
195
+ pass
196
+ except Exception:
197
+ pass
198
+
199
+ logger.error(
200
+ error_msg,
201
+ extra={
202
+ "http_status_code": status_code,
203
+ "http_response": response_text[:500] if response_text else "",
204
+ },
205
+ )
206
+ raise HTTPResponseError(error_msg, status_code, response_text)
207
+
208
+ elif status_code >= 500:
209
+ error_msg = f"Server error on {method} {url}: {status_code}"
210
+ logger.error(error_msg, extra={"http_status_code": status_code})
211
+ raise HTTPResponseError(error_msg, status_code)
212
+
213
+ # Synchronous methods
214
+
215
+ def get(self, endpoint: str, **kwargs) -> requests.Response:
216
+ """Perform GET request."""
217
+ return self.request("GET", endpoint, **kwargs)
218
+
219
+ def post(self, endpoint: str, **kwargs) -> requests.Response:
220
+ """Perform POST request."""
221
+ return self.request("POST", endpoint, **kwargs)
222
+
223
+ def put(self, endpoint: str, **kwargs) -> requests.Response:
224
+ """Perform PUT request."""
225
+ return self.request("PUT", endpoint, **kwargs)
226
+
227
+ def delete(self, endpoint: str, **kwargs) -> requests.Response:
228
+ """Perform DELETE request."""
229
+ return self.request("DELETE", endpoint, **kwargs)
230
+
231
+ def patch(self, endpoint: str, **kwargs) -> requests.Response:
232
+ """Perform PATCH request."""
233
+ return self.request("PATCH", endpoint, **kwargs)
234
+
235
+ def request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
236
+ """Perform synchronous HTTP request with error handling.
237
+
238
+ Args:
239
+ method: HTTP method
240
+ endpoint: URL endpoint
241
+ **kwargs: Additional arguments passed to requests
242
+
243
+ Returns:
244
+ Response object
245
+
246
+ Raises:
247
+ HTTPTimeoutError: On timeout
248
+ HTTPConnectionError: On connection failure
249
+ HTTPResponseError: On error response
250
+ """
251
+ url = self._build_url(endpoint)
252
+
253
+ # Set default timeout if not provided
254
+ if "timeout" not in kwargs:
255
+ kwargs["timeout"] = self.timeout
256
+
257
+ # Merge headers
258
+ if "headers" in kwargs:
259
+ headers = {**self.default_headers, **kwargs["headers"]}
260
+ kwargs["headers"] = headers
261
+
262
+ self._log_request(method, url, **kwargs)
263
+ start_time = time.time()
264
+
265
+ try:
266
+ response = self._sync_session.request(method, url, **kwargs)
267
+ duration = time.time() - start_time
268
+ self._log_response(method, url, response.status_code, duration)
269
+
270
+ # Check for HTTP errors
271
+ response.raise_for_status()
272
+
273
+ return response
274
+
275
+ except requests.TimeoutException as e:
276
+ logger.error(f"Timeout on {method} {url}: {e}")
277
+ raise HTTPTimeoutError(f"Request timeout on {method} {url}") from e
278
+
279
+ except requests.ConnectError as e:
280
+ logger.error(f"Connection error on {method} {url}: {e}")
281
+ raise HTTPConnectionError(f"Connection failed on {method} {url}") from e
282
+
283
+ except requests.HTTPStatusError as e:
284
+ # Handle error responses
285
+ if e.response is not None:
286
+ self._handle_response_error(e.response, method, url)
287
+ raise
288
+
289
+ except Exception as e:
290
+ logger.error(f"Unexpected error on {method} {url}: {e}")
291
+ raise HTTPClientError(f"Unexpected error on {method} {url}: {e}") from e
292
+
293
+ # Asynchronous methods
294
+
295
+ async def async_get(self, endpoint: str, **kwargs) -> Any:
296
+ """Perform async GET request."""
297
+ return await self.async_request("GET", endpoint, **kwargs)
298
+
299
+ async def async_post(self, endpoint: str, **kwargs) -> Any:
300
+ """Perform async POST request."""
301
+ return await self.async_request("POST", endpoint, **kwargs)
302
+
303
+ async def async_put(self, endpoint: str, **kwargs) -> Any:
304
+ """Perform async PUT request."""
305
+ return await self.async_request("PUT", endpoint, **kwargs)
306
+
307
+ async def async_delete(self, endpoint: str, **kwargs) -> Any:
308
+ """Perform async DELETE request."""
309
+ return await self.async_request("DELETE", endpoint, **kwargs)
310
+
311
+ async def async_patch(self, endpoint: str, **kwargs) -> Any:
312
+ """Perform async PATCH request."""
313
+ return await self.async_request("PATCH", endpoint, **kwargs)
314
+
315
+ async def async_request(self, method: str, endpoint: str, **kwargs) -> dict[str, Any]:
316
+ """Perform asynchronous HTTP request with error handling and retries.
317
+
318
+ Args:
319
+ method: HTTP method
320
+ endpoint: URL endpoint
321
+ **kwargs: Additional arguments passed to aiohttp
322
+
323
+ Returns:
324
+ Response data as dictionary
325
+
326
+ Raises:
327
+ HTTPTimeoutError: On timeout
328
+ HTTPConnectionError: On connection failure
329
+ HTTPResponseError: On error response
330
+ """
331
+ url = self._build_url(endpoint)
332
+ session = await self._ensure_async_session()
333
+ assert session is not None, "HTTP session must be available"
334
+
335
+ # Merge headers
336
+ headers = {**self.default_headers}
337
+ if "headers" in kwargs:
338
+ headers.update(kwargs["headers"])
339
+ kwargs["headers"] = headers
340
+ else:
341
+ kwargs["headers"] = headers
342
+
343
+ self._log_request(method, url, **kwargs)
344
+
345
+ # Implement retry logic
346
+ last_exception = None
347
+ for attempt in range(self.retry_config.total + 1):
348
+ if attempt > 0:
349
+ # Exponential backoff
350
+ sleep_time = self.retry_config.backoff_factor * (2 ** (attempt - 1))
351
+ logger.debug(f"Retrying {method} {url} after {sleep_time}s (attempt {attempt + 1})")
352
+ await asyncio.sleep(sleep_time)
353
+
354
+ start_time = time.time()
355
+
356
+ try:
357
+ async with session.request(method, url, **kwargs) as response:
358
+ duration = time.time() - start_time
359
+ self._log_response(method, url, response.status, duration)
360
+
361
+ # Check status code
362
+ if response.status >= 400:
363
+ response_text = await response.text()
364
+ if (
365
+ response.status in self.retry_config.status_forcelist
366
+ and attempt < self.retry_config.total
367
+ ):
368
+ # Retry on specific status codes
369
+ last_exception = HTTPResponseError(
370
+ f"Server error on {method} {url}: {response.status}",
371
+ response.status,
372
+ response_text,
373
+ )
374
+ continue
375
+
376
+ # Non-retryable error
377
+ error_msg = f"{'Client' if response.status < 500 else 'Server'} error on {method} {url}: {response.status}"
378
+ logger.error(
379
+ error_msg,
380
+ extra={
381
+ "http_status_code": response.status,
382
+ "http_response": (response_text[:500] if response_text else ""),
383
+ },
384
+ )
385
+ raise HTTPResponseError(error_msg, response.status, response_text)
386
+
387
+ # Success - return response data
388
+ try:
389
+ return await response.json()
390
+ except Exception:
391
+ # If JSON parsing fails, return text
392
+ text = await response.text()
393
+ return {"text": text, "status": response.status}
394
+
395
+ except TimeoutError as e:
396
+ logger.error(f"Timeout on {method} {url}: {e}")
397
+ last_exception = HTTPTimeoutError(f"Request timeout on {method} {url}")
398
+
399
+ except Exception as e:
400
+ if HAS_AIOHTTP and ClientError and isinstance(e, ClientError):
401
+ logger.error(f"Client error on {method} {url}: {e}")
402
+ last_exception = HTTPConnectionError(
403
+ f"Connection failed on {method} {url}: {e}"
404
+ )
405
+ else:
406
+ logger.error(f"Unexpected error on {method} {url}: {e}")
407
+ last_exception = HTTPClientError(f"Unexpected error on {method} {url}: {e}")
408
+
409
+ # All retries failed
410
+ if last_exception:
411
+ raise last_exception
412
+ else:
413
+ raise HTTPClientError(
414
+ f"Failed to complete request after {self.retry_config.total + 1} attempts"
415
+ )
416
+
417
+ def close(self):
418
+ """Close all sessions.
419
+
420
+ For async sessions, attempts to close properly by detecting the event loop.
421
+ For guaranteed async cleanup, use aclose() instead from async context.
422
+ """
423
+ self._sync_session.close()
424
+ if self._async_session:
425
+ try:
426
+ # Try to get running loop and schedule proper cleanup
427
+ loop = asyncio.get_running_loop()
428
+ # Schedule cleanup task - will be awaited by event loop
429
+ loop.create_task(self._async_session.close())
430
+ except RuntimeError:
431
+ # No running loop - try synchronous close via new loop
432
+ try:
433
+ asyncio.run(self._async_session.close())
434
+ except RuntimeError:
435
+ # Loop already running in another thread or closed
436
+ # Last resort: mark session as needing cleanup
437
+ logger.warning(
438
+ "Could not properly close async session - prefer using aclose() from async context"
439
+ )
440
+
441
+ async def aclose(self):
442
+ """Async close all sessions."""
443
+ self._sync_session.close()
444
+ if self._async_session:
445
+ await self._async_session.close()
446
+
447
+ def __enter__(self):
448
+ """Context manager entry."""
449
+ return self
450
+
451
+ def __exit__(self, exc_type, exc_val, exc_tb):
452
+ """Context manager exit."""
453
+ self.close()
454
+
455
+ async def __aenter__(self):
456
+ """Async context manager entry."""
457
+ return self
458
+
459
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
460
+ """Async context manager exit."""
461
+ await self.aclose()
462
+
463
+
464
+ # Convenience functions for one-off requests
465
+
466
+
467
+ def get(url: str, **kwargs) -> requests.Response:
468
+ """Perform one-off GET request with default retry logic."""
469
+ with HTTPClient() as client:
470
+ return client.get(url, **kwargs)
471
+
472
+
473
+ def post(url: str, **kwargs) -> requests.Response:
474
+ """Perform one-off POST request with default retry logic."""
475
+ with HTTPClient() as client:
476
+ return client.post(url, **kwargs)
477
+
478
+
479
+ async def async_get(url: str, **kwargs) -> dict[str, Any]:
480
+ """Perform one-off async GET request with default retry logic."""
481
+ async with HTTPClient() as client:
482
+ return await client.async_get(url, **kwargs)
483
+
484
+
485
+ async def async_post(url: str, **kwargs) -> dict[str, Any]:
486
+ """Perform one-off async POST request with default retry logic."""
487
+ async with HTTPClient() as client:
488
+ return await client.async_post(url, **kwargs)
489
+
490
+
491
+ # Decorator for automatic retry on specific functions
492
+
493
+
494
+ def with_http_retry(
495
+ retries: int = 3,
496
+ backoff_factor: float = 0.3,
497
+ exceptions: tuple = (HTTPTimeoutError, HTTPConnectionError),
498
+ ):
499
+ """Decorator to add retry logic to functions making HTTP calls.
500
+
501
+ Args:
502
+ retries: Number of retry attempts
503
+ backoff_factor: Backoff multiplier between retries
504
+ exceptions: Tuple of exceptions to retry on
505
+ """
506
+
507
+ def decorator(func):
508
+ @wraps(func)
509
+ def wrapper(*args, **kwargs):
510
+ last_exception = None
511
+ for attempt in range(retries + 1):
512
+ if attempt > 0:
513
+ sleep_time = backoff_factor * (2 ** (attempt - 1))
514
+ logger.debug(
515
+ f"Retrying {func.__name__} after {sleep_time}s (attempt {attempt + 1})"
516
+ )
517
+ time.sleep(sleep_time)
518
+
519
+ try:
520
+ return func(*args, **kwargs)
521
+ except exceptions as e:
522
+ logger.warning(f"Retry-able error in {func.__name__}: {e}")
523
+ last_exception = e
524
+ except Exception:
525
+ # Non-retryable exception
526
+ raise
527
+
528
+ # All retries exhausted
529
+ if last_exception:
530
+ raise last_exception
531
+
532
+ @wraps(func)
533
+ async def async_wrapper(*args, **kwargs):
534
+ last_exception = None
535
+ for attempt in range(retries + 1):
536
+ if attempt > 0:
537
+ sleep_time = backoff_factor * (2 ** (attempt - 1))
538
+ logger.debug(
539
+ f"Retrying {func.__name__} after {sleep_time}s (attempt {attempt + 1})"
540
+ )
541
+ await asyncio.sleep(sleep_time)
542
+
543
+ try:
544
+ return await func(*args, **kwargs)
545
+ except exceptions as e:
546
+ logger.warning(f"Retry-able error in {func.__name__}: {e}")
547
+ last_exception = e
548
+ except Exception:
549
+ # Non-retryable exception
550
+ raise
551
+
552
+ # All retries exhausted
553
+ if last_exception:
554
+ raise last_exception
555
+
556
+ # Return appropriate wrapper based on function type
557
+ if inspect.iscoroutinefunction(func):
558
+ return async_wrapper
559
+ else:
560
+ return wrapper
561
+
562
+ return decorator