core-https 1.1.7__tar.gz → 2.0.0__tar.gz

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 (39) hide show
  1. core_https-2.0.0/PKG-INFO +112 -0
  2. core_https-2.0.0/README.rst +74 -0
  3. {core_https-1.1.7 → core_https-2.0.0}/core_https/__init__.py +1 -1
  4. core_https-2.0.0/core_https/exceptions.py +152 -0
  5. core_https-2.0.0/core_https/requesters/aiohttp_.py +298 -0
  6. core_https-2.0.0/core_https/requesters/base.py +391 -0
  7. {core_https-1.1.7 → core_https-2.0.0}/core_https/requesters/requests_.py +17 -11
  8. {core_https-1.1.7 → core_https-2.0.0}/core_https/requesters/urllib3_.py +13 -15
  9. core_https-2.0.0/core_https/tests/__init__.py +0 -0
  10. core_https-2.0.0/core_https/tests/aiohttp_.py +268 -0
  11. core_https-2.0.0/core_https/tests/base.py +88 -0
  12. core_https-2.0.0/core_https/tests/decorators.py +325 -0
  13. core_https-2.0.0/core_https/tests/requests_.py +215 -0
  14. core_https-2.0.0/core_https/tests/urllib3_.py +353 -0
  15. core_https-2.0.0/core_https/utils.py +364 -0
  16. core_https-2.0.0/core_https.egg-info/PKG-INFO +112 -0
  17. {core_https-1.1.7 → core_https-2.0.0}/core_https.egg-info/SOURCES.txt +2 -1
  18. core_https-2.0.0/core_https.egg-info/requires.txt +14 -0
  19. {core_https-1.1.7 → core_https-2.0.0}/pyproject.toml +16 -17
  20. core_https-1.1.7/PKG-INFO +0 -76
  21. core_https-1.1.7/README.md +0 -34
  22. core_https-1.1.7/core_https/exceptions.py +0 -67
  23. core_https-1.1.7/core_https/requesters/aiohttp_.py +0 -178
  24. core_https-1.1.7/core_https/requesters/base.py +0 -161
  25. core_https-1.1.7/core_https/tests/aiohttp_.py +0 -106
  26. core_https-1.1.7/core_https/tests/base.py +0 -12
  27. core_https-1.1.7/core_https/tests/decorators.py +0 -180
  28. core_https-1.1.7/core_https/tests/requests_.py +0 -75
  29. core_https-1.1.7/core_https/tests/urllib3_.py +0 -189
  30. core_https-1.1.7/core_https/utils.py +0 -159
  31. core_https-1.1.7/core_https.egg-info/PKG-INFO +0 -76
  32. core_https-1.1.7/core_https.egg-info/requires.txt +0 -24
  33. {core_https-1.1.7 → core_https-2.0.0}/LICENSE +0 -0
  34. /core_https-1.1.7/core_https/requesters/__init__.py → /core_https-2.0.0/core_https/py.typed +0 -0
  35. {core_https-1.1.7/core_https/tests → core_https-2.0.0/core_https/requesters}/__init__.py +0 -0
  36. {core_https-1.1.7 → core_https-2.0.0}/core_https.egg-info/dependency_links.txt +0 -0
  37. {core_https-1.1.7 → core_https-2.0.0}/core_https.egg-info/top_level.txt +0 -0
  38. {core_https-1.1.7 → core_https-2.0.0}/setup.cfg +0 -0
  39. {core_https-1.1.7 → core_https-2.0.0}/setup.py +0 -0
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: core-https
3
+ Version: 2.0.0
4
+ Summary: This project/library contains common elements related to HTTP & API services...
5
+ Author-email: Alejandro Cora González <alek.cora.glez@gmail.com>
6
+ Maintainer: Alejandro Cora González
7
+ License: MIT
8
+ Project-URL: Homepage, https://gitlab.com/bytecode-solutions/core/core-https
9
+ Project-URL: Repository, https://gitlab.com/bytecode-solutions/core/core-https
10
+ Project-URL: Documentation, https://core-https.readthedocs.io/en/latest/
11
+ Project-URL: Issues, https://gitlab.com/bytecode-solutions/core/core-https/-/issues
12
+ Project-URL: Changelog, https://gitlab.com/bytecode-solutions/core/core-https/-/blob/master/CHANGELOG.md
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Development Status :: 5 - Production/Stable
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: Utilities
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/x-rst
27
+ License-File: LICENSE
28
+ Requires-Dist: aiohttp<4.0.0,>=3.12.0; python_version >= "3.9"
29
+ Requires-Dist: core-mixins>=2.1.0
30
+ Requires-Dist: core-tests>=2.0.1
31
+ Requires-Dist: requests<3.0.0,>=2.32.3; python_version >= "3.9"
32
+ Requires-Dist: typing-extensions>=4.8.0; python_version >= "3.9" and python_version < "3.11"
33
+ Requires-Dist: urllib3<3.0.0,>=2.2.3; python_version >= "3.9"
34
+ Provides-Extra: dev
35
+ Requires-Dist: core-dev-tools>=1.0.1; extra == "dev"
36
+ Requires-Dist: types-requests>=2.32.0.20250602; extra == "dev"
37
+ Dynamic: license-file
38
+
39
+ core-https
40
+ ===============================================================================
41
+
42
+ This project/library contains common elements related to HTTP...
43
+
44
+ ===============================================================================
45
+
46
+ .. image:: https://img.shields.io/pypi/pyversions/core-https.svg
47
+ :target: https://pypi.org/project/core-https/
48
+ :alt: Python Versions
49
+
50
+ .. image:: https://img.shields.io/badge/license-MIT-blue.svg
51
+ :target: https://gitlab.com/bytecode-solutions/core/core-https/-/blob/main/LICENSE
52
+ :alt: License
53
+
54
+ .. image:: https://gitlab.com/bytecode-solutions/core/core-https/badges/release/pipeline.svg
55
+ :target: https://gitlab.com/bytecode-solutions/core/core-https/-/pipelines
56
+ :alt: Pipeline Status
57
+
58
+ .. image:: https://readthedocs.org/projects/core-https/badge/?version=latest
59
+ :target: https://readthedocs.org/projects/core-https/
60
+ :alt: Docs Status
61
+
62
+ .. image:: https://img.shields.io/badge/security-bandit-yellow.svg
63
+ :target: https://github.com/PyCQA/bandit
64
+ :alt: Security
65
+
66
+ |
67
+
68
+ Execution Environment
69
+ ---------------------------------------
70
+
71
+ Install libraries
72
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
73
+
74
+ .. code-block:: shell
75
+
76
+ pip install --upgrade pip
77
+ pip install virtualenv
78
+ ..
79
+
80
+ Create the Python Virtual Environment.
81
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
82
+
83
+ .. code-block:: shell
84
+
85
+ virtualenv --python={{python-version}} .venv
86
+ virtualenv --python=python3.11 .venv
87
+ ..
88
+
89
+ Activate the Virtual Environment.
90
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
91
+
92
+ .. code-block:: shell
93
+
94
+ source .venv/bin/activate
95
+ ..
96
+
97
+ Install required libraries.
98
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
99
+
100
+ .. code-block:: shell
101
+
102
+ pip install .
103
+ ..
104
+
105
+ Check tests and coverage.
106
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
107
+
108
+ .. code-block:: shell
109
+
110
+ python manager.py run-tests
111
+ python manager.py run-coverage
112
+ ..
@@ -0,0 +1,74 @@
1
+ core-https
2
+ ===============================================================================
3
+
4
+ This project/library contains common elements related to HTTP...
5
+
6
+ ===============================================================================
7
+
8
+ .. image:: https://img.shields.io/pypi/pyversions/core-https.svg
9
+ :target: https://pypi.org/project/core-https/
10
+ :alt: Python Versions
11
+
12
+ .. image:: https://img.shields.io/badge/license-MIT-blue.svg
13
+ :target: https://gitlab.com/bytecode-solutions/core/core-https/-/blob/main/LICENSE
14
+ :alt: License
15
+
16
+ .. image:: https://gitlab.com/bytecode-solutions/core/core-https/badges/release/pipeline.svg
17
+ :target: https://gitlab.com/bytecode-solutions/core/core-https/-/pipelines
18
+ :alt: Pipeline Status
19
+
20
+ .. image:: https://readthedocs.org/projects/core-https/badge/?version=latest
21
+ :target: https://readthedocs.org/projects/core-https/
22
+ :alt: Docs Status
23
+
24
+ .. image:: https://img.shields.io/badge/security-bandit-yellow.svg
25
+ :target: https://github.com/PyCQA/bandit
26
+ :alt: Security
27
+
28
+ |
29
+
30
+ Execution Environment
31
+ ---------------------------------------
32
+
33
+ Install libraries
34
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
35
+
36
+ .. code-block:: shell
37
+
38
+ pip install --upgrade pip
39
+ pip install virtualenv
40
+ ..
41
+
42
+ Create the Python Virtual Environment.
43
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
44
+
45
+ .. code-block:: shell
46
+
47
+ virtualenv --python={{python-version}} .venv
48
+ virtualenv --python=python3.11 .venv
49
+ ..
50
+
51
+ Activate the Virtual Environment.
52
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
53
+
54
+ .. code-block:: shell
55
+
56
+ source .venv/bin/activate
57
+ ..
58
+
59
+ Install required libraries.
60
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
61
+
62
+ .. code-block:: shell
63
+
64
+ pip install .
65
+ ..
66
+
67
+ Check tests and coverage.
68
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
69
+
70
+ .. code-block:: shell
71
+
72
+ python manager.py run-tests
73
+ python manager.py run-coverage
74
+ ..
@@ -4,7 +4,7 @@ try:
4
4
  from enum import StrEnum
5
5
 
6
6
  except ImportError:
7
- from core_mixins.compatibility import StrEnum
7
+ from core_mixins.compatibility import StrEnum # type: ignore
8
8
 
9
9
 
10
10
  class StatusInfo(StrEnum):
@@ -0,0 +1,152 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """
4
+ HTTP client exception hierarchy for the core_https library.
5
+
6
+ This module provides a comprehensive set of exception classes for handling
7
+ HTTP-related errors in a structured and consistent manner. The exception
8
+ hierarchy is designed to allow for fine-grained error handling while
9
+ maintaining compatibility with standard HTTP status codes.
10
+
11
+ Exception Hierarchy:
12
+ Exception
13
+ └── InternalServerError (base for all HTTP errors)
14
+ ├── ServiceException (handled service errors)
15
+ │ ├── AuthenticationException (401 Unauthorized)
16
+ │ ├── AuthorizationException (403 Forbidden)
17
+ │ └── RateLimitException (429 Too Many Requests)
18
+ └── RetryableException (errors that should trigger retries)
19
+
20
+ Usage Example:
21
+ Basic exception handling::
22
+
23
+ from core_https.exceptions import AuthenticationException, RateLimitException
24
+
25
+ try:
26
+ response = requester.request(url="https://api.example.com")
27
+ except AuthenticationException as e:
28
+ logger.error(f"Authentication failed: {e.details}")
29
+ # Handle authentication error
30
+ except RateLimitException as e:
31
+ logger.warning(f"Rate limited: {e.details}")
32
+ # Implement backoff strategy
33
+ except ServiceException as e:
34
+ logger.error(f"Service error {e.status_code}: {e.details}")
35
+ # Handle other service errors
36
+
37
+ Error information extraction::
38
+
39
+ try:
40
+ # HTTP request that fails
41
+ pass
42
+ except InternalServerError as e:
43
+ error_info = e.get_error_info()
44
+ # {'type': 'AuthenticationException', 'details': 'Invalid API key'}
45
+
46
+ See Also:
47
+ - core_https.requesters.base.IRequester: Base requester that raises these exceptions
48
+ - HTTP status codes: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
49
+ """
50
+
51
+ from typing import Dict
52
+
53
+
54
+ class InternalServerError(Exception):
55
+ """
56
+ Base class for all HTTP-related exceptions in the core_https library.
57
+
58
+ This exception serves as the root of the HTTP exception hierarchy and handles
59
+ unhandled errors that occur during HTTP operations. It provides structured
60
+ error information including HTTP status codes and detailed error messages.
61
+
62
+ Attributes:
63
+ status_code: HTTP status code associated with the error
64
+ details: Detailed error message or description
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ status_code: int,
70
+ details: str,
71
+ *args,
72
+ ) -> None:
73
+ super().__init__(*args)
74
+ self.status_code = status_code
75
+ self.details = details
76
+
77
+ def get_error_info(self) -> Dict[str, str]:
78
+ """
79
+ Get structured error information for logging or serialization.
80
+ :returns: Dictionary containing error type and details.
81
+ """
82
+
83
+ return {
84
+ "type": self.__class__.__name__,
85
+ "details": self.details,
86
+ }
87
+
88
+
89
+ class ServiceException(InternalServerError):
90
+ """Exception caused for handled errors within the service"""
91
+
92
+
93
+ class AuthenticationException(ServiceException):
94
+ """Exception caused for authentication [401] issues"""
95
+
96
+ def __init__(
97
+ self,
98
+ status_code: int = 401,
99
+ details: str = "Unauthorized",
100
+ ) -> None:
101
+ super().__init__(
102
+ status_code=status_code,
103
+ details=details,
104
+ )
105
+
106
+
107
+ class AuthorizationException(ServiceException):
108
+ """Exception caused for authorization [403] issues"""
109
+
110
+ def __init__(
111
+ self,
112
+ status_code: int = 403,
113
+ details: str = "Forbidden",
114
+ ) -> None:
115
+ super().__init__(
116
+ status_code=status_code,
117
+ details=details,
118
+ )
119
+
120
+
121
+ class RateLimitException(ServiceException):
122
+ """
123
+ Exception caused [429] when a client has sent too many requests
124
+ to a server within a given time frame.
125
+ """
126
+
127
+ def __init__(
128
+ self,
129
+ status_code: int = 429,
130
+ details: str = "Too Many Requests",
131
+ ) -> None:
132
+ super().__init__(
133
+ status_code=status_code,
134
+ details=details,
135
+ )
136
+
137
+
138
+ class RetryableException(InternalServerError):
139
+ """
140
+ Exception for HTTP errors that should trigger retry mechanisms.
141
+
142
+ This exception represents temporary failures that are likely to succeed
143
+ if retried after a short delay. Common retryable status codes include:
144
+ - 429 Too Many Requests (rate limiting)
145
+ - 502 Bad Gateway (temporary server issue)
146
+ - 503 Service Unavailable (server overload)
147
+ - 504 Gateway Timeout (temporary timeout)
148
+
149
+ Example:
150
+ raise RetryableException(status_code=503, details="Service temporarily unavailable")
151
+ raise RetryableException(status_code=502, details="Bad gateway from upstream")
152
+ """
@@ -0,0 +1,298 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any
7
+ from typing import Dict
8
+ from typing import Optional
9
+
10
+ try:
11
+ from typing import Self
12
+
13
+ except ImportError:
14
+ from typing_extensions import Self
15
+
16
+ from aiohttp import (
17
+ ClientResponse,
18
+ ClientResponseError,
19
+ ClientSession,
20
+ ClientTimeout,
21
+ TCPConnector,
22
+ )
23
+
24
+ from .base import IRequester
25
+ from .base import HTTPMethod
26
+
27
+
28
+ class AioHttpRequester(IRequester):
29
+ """
30
+ Asynchronous HTTP requester implementation using aiohttp.
31
+
32
+ This class provides an async HTTP client interface using the aiohttp library.
33
+ It supports automatic session management, configurable retry logic with exponential
34
+ backoff, connection pooling, and comprehensive error handling.
35
+
36
+ The requester can work with externally provided ClientSession objects or create
37
+ and manage its own session internally. It implements the async context manager
38
+ protocol for convenient resource cleanup.
39
+
40
+ Features:
41
+ - Automatic session creation and management
42
+ - Configurable retry logic with exponential backoff
43
+ - Connection pooling with configurable limits
44
+ - Comprehensive HTTP exception mapping
45
+ - Support for custom timeouts per request
46
+ - Context manager support for resource cleanup
47
+
48
+ .. code-block:: python
49
+
50
+ import aiohttp
51
+ from core_https.requesters.aiohttp_ import AioHttpRequester
52
+ from core_https.utils import HTTPMethod
53
+
54
+ requester: AioHttpRequester = AioHttpRequester(raise_for_status=True)
55
+
56
+ async def get():
57
+ # This is optional as the client creates one session for you if not provided.
58
+ session = aiohttp.ClientSession()
59
+
60
+ try:
61
+ response = await requester.request(
62
+ method=HTTPMethod.GET,
63
+ session=session,
64
+ url=url,
65
+ params={
66
+ "x-api-key": "..."
67
+ })
68
+
69
+ return await response.text()
70
+
71
+ except Exception as error:
72
+ pass
73
+
74
+ finally:
75
+ await session.close()
76
+
77
+ res = asyncio.run(get())
78
+ print(res)
79
+ ..
80
+ """
81
+
82
+ def __init__(
83
+ self,
84
+ session: Optional[ClientSession] = None,
85
+ retries: Optional[int] = 3,
86
+ **kwargs,
87
+ ) -> None:
88
+ """
89
+ Initialize the AioHttpRequester.
90
+
91
+ Args:
92
+ session: Optional pre-configured aiohttp ClientSession to use for requests.
93
+ If not provided, a new session will be created automatically with
94
+ the configured timeout and connection limits. When providing a custom
95
+ session, you are responsible for closing it.
96
+
97
+ retries: Number of retry attempts for failed requests. Defaults to 3.
98
+ Set to 0 to disable retries completely. Only applies to retryable
99
+ errors like network timeouts and server errors (5xx status codes).
100
+
101
+ **kwargs: Additional arguments passed to the base IRequester class,
102
+ including encoding, raise_for_status, backoff_factor, timeout,
103
+ connector_limit, and connector_limit_per_host.
104
+
105
+ Note:
106
+ The timeout specified in kwargs becomes the default session timeout.
107
+ Individual requests can override this using the timeout parameter
108
+ in the request() method.
109
+ """
110
+
111
+ super().__init__(**kwargs)
112
+
113
+ self._session = session
114
+ self._session_lock = asyncio.Lock()
115
+ self._owns_session = session is None
116
+ self._timeout = ClientTimeout(total=self.timeout)
117
+ self.retries = retries
118
+
119
+ async def __aenter__(self) -> Self:
120
+ await self._ensure_session()
121
+ return self
122
+
123
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
124
+ await self.close()
125
+
126
+ @classmethod
127
+ def engine(cls) -> str:
128
+ return "aiohttp"
129
+
130
+ async def _ensure_session(self) -> ClientSession:
131
+ """
132
+ Ensure a ClientSession exists, creating one if necessary.
133
+
134
+ This method implements a thread-safe lazy initialization pattern using
135
+ double-checked locking to ensure only one session is created even when
136
+ called concurrently from multiple coroutines.
137
+
138
+ Returns:
139
+ ClientSession: The active aiohttp ClientSession instance.
140
+
141
+ Note:
142
+ If a session was provided in the constructor, it will be returned as-is.
143
+ If no session was provided, a new one will be created with the configured
144
+ timeout and connection limits.
145
+
146
+ Thread Safety:
147
+ This method is safe to call concurrently from multiple coroutines.
148
+ The double-check locking pattern ensures only one session is created.
149
+ """
150
+
151
+ if self._session is None:
152
+ async with self._session_lock:
153
+ if self._session is None: # Double-check after acquiring lock...
154
+ self._session = ClientSession(
155
+ timeout=self._timeout,
156
+ connector=TCPConnector(
157
+ limit=self.connector_limit,
158
+ limit_per_host=self.connector_limit_per_host,
159
+ ),
160
+ )
161
+
162
+ self._owns_session = True
163
+
164
+ return self._session
165
+
166
+ async def request(
167
+ self,
168
+ url: str,
169
+ method: HTTPMethod = HTTPMethod.GET,
170
+ headers: Optional[Dict[str, Any]] = None,
171
+ retries: Optional[int] = None,
172
+ backoff_factor: Optional[float] = None,
173
+ session: Optional[ClientSession] = None,
174
+ params: Optional[Dict[str, Any]] = None,
175
+ timeout: Optional[float] = None,
176
+ **kwargs,
177
+ ) -> ClientResponse:
178
+ """
179
+ Make an asynchronous HTTP request with retry logic.
180
+
181
+ This method performs HTTP requests with automatic retry functionality,
182
+ exponential backoff, and comprehensive error handling. Failed requests
183
+ are retried based on the configured retry policy.
184
+
185
+ Args:
186
+ url: The target URL for the HTTP request.
187
+
188
+ method: HTTP method to use. Defaults to GET. Supports all standard
189
+ HTTP methods (GET, POST, PUT, DELETE, etc.).
190
+
191
+ headers: Optional HTTP headers to include in the request. These will
192
+ be merged with any session-level headers.
193
+
194
+ retries: Number of retry attempts for this specific request.
195
+ If not provided, uses the instance-level retry setting.
196
+ Set to 0 to disable retries for this request.
197
+
198
+ backoff_factor: Multiplier for exponential backoff between retries.
199
+ The actual delay is calculated as: backoff_factor * attempt_number.
200
+ If not provided, uses the instance-level setting or defaults to 0.5.
201
+
202
+ session: Optional ClientSession to use for this request.
203
+ If not provided, uses the requester's session (creating one if necessary).
204
+ Useful for per-request session customization.
205
+
206
+ params: URL query parameters to include in the request.
207
+ Will be properly URL-encoded and appended to the URL.
208
+
209
+ timeout: Request timeout in seconds for this specific request.
210
+ Overrides the instance-level timeout setting.
211
+ Set to None to use the default timeout.
212
+
213
+ **kwargs: Additional parameters passed to aiohttp's request method.
214
+ Common options include: json, data, cookies, ssl, proxy, etc.
215
+ See aiohttp.ClientSession.request documentation for full list.
216
+
217
+ Returns:
218
+ ClientResponse: The aiohttp response object. Use methods like
219
+ .json(), .text(), .read() to extract the response content.
220
+
221
+ Raises:
222
+ AuthenticationException: For 401 Unauthorized responses.
223
+ AuthorizationException: For 403 Forbidden responses.
224
+ RateLimitException: For 429 Too Many Requests (when retries exhausted).
225
+ RetryableException: For 502/503/504 server errors (when retries exhausted).
226
+ ServiceException: For other 4xx client errors.
227
+ InternalServerError: For 5xx server errors (when retries exhausted).
228
+
229
+ Note:
230
+ The retry logic only applies to network errors and server errors (5xx).
231
+ Client errors (4xx) are not retried, except for 429 (rate limit)
232
+ which may be retried based on the configured policy.
233
+ """
234
+
235
+ session_ = session or await self._ensure_session()
236
+ kwargs_ = kwargs.copy()
237
+
238
+ if timeout is not None:
239
+ kwargs_["timeout"] = ClientTimeout(total=timeout)
240
+
241
+ retries = retries if retries is not None else self.retries
242
+ if retries is None:
243
+ retries = 3
244
+
245
+ backoff_factor = (
246
+ backoff_factor
247
+ if backoff_factor is not None
248
+ else self.backoff_factor
249
+ if self.backoff_factor is not None
250
+ else 0.5
251
+ )
252
+
253
+ attempts = 0
254
+
255
+ while True:
256
+ attempts += 1
257
+
258
+ try:
259
+ response = await session_.request(
260
+ method=str(method),
261
+ url=url,
262
+ headers=headers,
263
+ params=params,
264
+ **kwargs_,
265
+ )
266
+
267
+ if self.raise_for_status:
268
+ response.raise_for_status()
269
+
270
+ return response
271
+
272
+ except ClientResponseError as error:
273
+ if attempts > retries:
274
+ self.raise_custom_exception(error.status, error.message)
275
+
276
+ await asyncio.sleep(backoff_factor * attempts)
277
+
278
+ async def close(self) -> None:
279
+ """
280
+ Close the internal session if it was created by this requester.
281
+
282
+ This method performs cleanup of the internal ClientSession, but only
283
+ if the session was created internally. If a custom session was provided
284
+ in the constructor, it will not be closed as the caller is responsible
285
+ for managing its lifecycle.
286
+
287
+ The session reference is always cleared after calling this method,
288
+ regardless of whether it was closed or not.
289
+
290
+ Note:
291
+ This method is automatically called when using the async context
292
+ manager protocol (__aexit__). Manual calling is only necessary
293
+ when not using the context manager pattern.
294
+ """
295
+
296
+ if self._session and self._owns_session:
297
+ await self._session.close()
298
+ self._session = None