atomhttp 1.0.0__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.
- atomhttp/__init__.py +76 -0
- atomhttp/adapters/__init__.py +4 -0
- atomhttp/adapters/http_adapter.py +102 -0
- atomhttp/adapters/mock_adapter.py +130 -0
- atomhttp/auth/__init__.py +4 -0
- atomhttp/auth/basic_auth.py +90 -0
- atomhttp/auth/bearer_auth.py +83 -0
- atomhttp/client.py +577 -0
- atomhttp/core/__init__.py +19 -0
- atomhttp/core/adapters.py +687 -0
- atomhttp/core/config.py +186 -0
- atomhttp/core/defaults.py +212 -0
- atomhttp/core/form_data.py +282 -0
- atomhttp/core/request.py +240 -0
- atomhttp/core/response.py +101 -0
- atomhttp/errors/__init__.py +13 -0
- atomhttp/errors/http_errors.py +142 -0
- atomhttp/interceptors/__init__.py +5 -0
- atomhttp/interceptors/manager.py +136 -0
- atomhttp/interceptors/request_interceptor.py +18 -0
- atomhttp/interceptors/response_interceptor.py +18 -0
- atomhttp/progress/__init__.py +3 -0
- atomhttp/progress/upload_progress.py +89 -0
- atomhttp/transforms/__init__.py +5 -0
- atomhttp/transforms/data_serializer.py +96 -0
- atomhttp/transforms/request_transform.py +96 -0
- atomhttp/transforms/response_transform.py +73 -0
- atomhttp/utils/__init__.py +5 -0
- atomhttp/utils/cookies.py +128 -0
- atomhttp/utils/helpers.py +111 -0
- atomhttp/utils/redirect.py +107 -0
- atomhttp-1.0.0.dist-info/METADATA +165 -0
- atomhttp-1.0.0.dist-info/RECORD +36 -0
- atomhttp-1.0.0.dist-info/WHEEL +5 -0
- atomhttp-1.0.0.dist-info/licenses/LICENSE +21 -0
- atomhttp-1.0.0.dist-info/top_level.txt +1 -0
atomhttp/core/request.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Request Handler Module
|
|
3
|
+
----------------------
|
|
4
|
+
Core request execution engine for AtomHTTP client.
|
|
5
|
+
|
|
6
|
+
This module contains the RequestHandler class which orchestrates the entire
|
|
7
|
+
request lifecycle including interceptor execution, request transformation,
|
|
8
|
+
adapter selection, response transformation, and comprehensive error handling.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import aiohttp
|
|
13
|
+
from typing import Optional, Any
|
|
14
|
+
from .config import RequestConfig
|
|
15
|
+
from .response import Response
|
|
16
|
+
from .adapters import HTTPAdapter
|
|
17
|
+
from ..transforms.request_transform import RequestTransformer
|
|
18
|
+
from ..transforms.response_transform import ResponseTransformer
|
|
19
|
+
from ..errors.http_errors import (
|
|
20
|
+
AtomHTTPRequestError,
|
|
21
|
+
AtomHTTPTimeoutError,
|
|
22
|
+
AtomHTTPNetworkError,
|
|
23
|
+
AtomHTTPError
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RequestHandler:
|
|
28
|
+
"""
|
|
29
|
+
Core request handler that orchestrates the entire HTTP request lifecycle.
|
|
30
|
+
|
|
31
|
+
This class is responsible for executing HTTP requests through a pipeline
|
|
32
|
+
of interceptors, transformers, and adapters. It manages the flow of
|
|
33
|
+
request execution from start to finish, applying all configured
|
|
34
|
+
middleware and transformations.
|
|
35
|
+
|
|
36
|
+
The request flow follows this sequence:
|
|
37
|
+
1. Request interceptors are applied (can modify config)
|
|
38
|
+
2. Request data transformation is applied
|
|
39
|
+
3. URL validation is performed
|
|
40
|
+
4. HTTP adapter is selected and request is sent
|
|
41
|
+
5. Response transformation is applied
|
|
42
|
+
6. Response interceptors are applied
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
defaults: Default configuration object for the client
|
|
46
|
+
interceptors (InterceptorManager): Manager for request/response interceptors
|
|
47
|
+
request_transformer (RequestTransformer): Handles request data transformation
|
|
48
|
+
response_transformer (ResponseTransformer): Handles response data transformation
|
|
49
|
+
default_adapter (HTTPAdapter): Default HTTP adapter for network requests
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, defaults, interceptor_manager):
|
|
53
|
+
"""
|
|
54
|
+
Initialize the request handler with defaults and interceptors.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
defaults: Default configuration object containing base settings
|
|
58
|
+
interceptor_manager: Manager instance holding registered interceptors
|
|
59
|
+
"""
|
|
60
|
+
self.defaults = defaults
|
|
61
|
+
self.interceptors = interceptor_manager
|
|
62
|
+
self.request_transformer = RequestTransformer()
|
|
63
|
+
self.response_transformer = ResponseTransformer()
|
|
64
|
+
self.default_adapter = HTTPAdapter()
|
|
65
|
+
|
|
66
|
+
async def execute(self, config: RequestConfig) -> Response:
|
|
67
|
+
"""
|
|
68
|
+
Execute a complete HTTP request through the full processing pipeline.
|
|
69
|
+
|
|
70
|
+
This method orchestrates the entire request execution process:
|
|
71
|
+
1. Apply request interceptors (modify config before request)
|
|
72
|
+
2. Transform request data (serialization, auth headers, etc.)
|
|
73
|
+
3. Validate URL format
|
|
74
|
+
4. Select and execute HTTP adapter (network request)
|
|
75
|
+
5. Transform response data
|
|
76
|
+
6. Apply response interceptors (modify response after request)
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
config (RequestConfig): Complete request configuration
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Response: Final response after all transformations and interceptors
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
AtomHTTPRequestError: Invalid request or URL
|
|
86
|
+
AtomHTTPTimeoutError: Request timeout exceeded
|
|
87
|
+
AtomHTTPNetworkError: Network connectivity issues
|
|
88
|
+
AtomHTTPError: Base exception for all AtomHTTP errors
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
# Step 1: Apply all registered request interceptors
|
|
92
|
+
# These can modify headers, add auth, log requests, etc.
|
|
93
|
+
modified_config = await self._apply_request_interceptors(config)
|
|
94
|
+
|
|
95
|
+
# Step 2: Transform request data (JSON serialization, auth headers, etc.)
|
|
96
|
+
transformed_config = self.request_transformer.transform(modified_config)
|
|
97
|
+
|
|
98
|
+
# Step 3: Validate that URL exists and has proper protocol
|
|
99
|
+
if not transformed_config.url:
|
|
100
|
+
raise AtomHTTPRequestError(
|
|
101
|
+
"URL is required",
|
|
102
|
+
request=transformed_config,
|
|
103
|
+
config=transformed_config
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Ensure URL uses HTTP or HTTPS protocol
|
|
107
|
+
if not (transformed_config.url.startswith('http://') or transformed_config.url.startswith('https://')):
|
|
108
|
+
raise AtomHTTPRequestError(
|
|
109
|
+
f"Invalid URL: {transformed_config.url}. URL must start with http:// or https://",
|
|
110
|
+
request=transformed_config,
|
|
111
|
+
config=transformed_config
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Step 4: Select adapter (custom or default) and execute request
|
|
115
|
+
adapter = transformed_config.adapter or self.default_adapter
|
|
116
|
+
response = await adapter.send(transformed_config)
|
|
117
|
+
|
|
118
|
+
# Step 5: Transform response data (parse JSON, modify structure, etc.)
|
|
119
|
+
transformed_response = self.response_transformer.transform(response)
|
|
120
|
+
|
|
121
|
+
# Step 6: Apply all registered response interceptors
|
|
122
|
+
final_response = await self._apply_response_interceptors(transformed_response)
|
|
123
|
+
|
|
124
|
+
return final_response
|
|
125
|
+
|
|
126
|
+
# Re-raise AtomHTTP errors without modification
|
|
127
|
+
except AtomHTTPError:
|
|
128
|
+
raise
|
|
129
|
+
|
|
130
|
+
# Convert asyncio timeout to AtomHTTP timeout error
|
|
131
|
+
except asyncio.TimeoutError:
|
|
132
|
+
error = AtomHTTPTimeoutError(
|
|
133
|
+
f"Request timeout after {config.timeout}s",
|
|
134
|
+
config=config,
|
|
135
|
+
request=config
|
|
136
|
+
)
|
|
137
|
+
error.code = 'ECONNABORTED'
|
|
138
|
+
raise error
|
|
139
|
+
|
|
140
|
+
# Convert aiohttp client errors to AtomHTTP network errors
|
|
141
|
+
except aiohttp.ClientError as e:
|
|
142
|
+
error = AtomHTTPNetworkError(str(e), config=config)
|
|
143
|
+
error.code = 'ERR_NETWORK'
|
|
144
|
+
raise error
|
|
145
|
+
|
|
146
|
+
# Convert any other exception to AtomHTTP request error
|
|
147
|
+
except Exception as e:
|
|
148
|
+
if not isinstance(e, AtomHTTPError):
|
|
149
|
+
error = AtomHTTPRequestError(str(e), request=config, config=config)
|
|
150
|
+
error.code = 'ERR_BAD_REQUEST'
|
|
151
|
+
raise error
|
|
152
|
+
raise
|
|
153
|
+
|
|
154
|
+
async def _apply_request_interceptors(self, config: RequestConfig) -> RequestConfig:
|
|
155
|
+
"""
|
|
156
|
+
Apply all registered request interceptors in sequence.
|
|
157
|
+
|
|
158
|
+
Request interceptors are executed in the order they were registered.
|
|
159
|
+
Each interceptor receives the current configuration and returns a
|
|
160
|
+
(potentially modified) configuration for the next interceptor.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
config (RequestConfig): Current request configuration
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
RequestConfig: Configuration after all interceptors have been applied
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
AtomHTTPRequestError: If any interceptor fails or returns invalid type
|
|
170
|
+
"""
|
|
171
|
+
for idx, interceptor in enumerate(self.interceptors.request_interceptors):
|
|
172
|
+
try:
|
|
173
|
+
# Execute interceptor (supports both sync and async functions)
|
|
174
|
+
result = interceptor(config)
|
|
175
|
+
if asyncio.iscoroutine(result):
|
|
176
|
+
config = await result
|
|
177
|
+
else:
|
|
178
|
+
config = result
|
|
179
|
+
|
|
180
|
+
# Validate interceptor return type
|
|
181
|
+
if not isinstance(config, RequestConfig):
|
|
182
|
+
raise AtomHTTPRequestError(
|
|
183
|
+
f"Request interceptor {idx} must return RequestConfig",
|
|
184
|
+
request=config,
|
|
185
|
+
config=config
|
|
186
|
+
)
|
|
187
|
+
except AtomHTTPError:
|
|
188
|
+
raise
|
|
189
|
+
except Exception as e:
|
|
190
|
+
# Wrap any interceptor exception in AtomHTTP error
|
|
191
|
+
raise AtomHTTPRequestError(
|
|
192
|
+
f"Request interceptor {idx} error: {str(e)}",
|
|
193
|
+
request=config,
|
|
194
|
+
config=config
|
|
195
|
+
)
|
|
196
|
+
return config
|
|
197
|
+
|
|
198
|
+
async def _apply_response_interceptors(self, response: Response) -> Response:
|
|
199
|
+
"""
|
|
200
|
+
Apply all registered response interceptors in sequence.
|
|
201
|
+
|
|
202
|
+
Response interceptors are executed in the order they were registered.
|
|
203
|
+
Each interceptor receives the current response and returns a
|
|
204
|
+
(potentially modified) response for the next interceptor.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
response (Response): Current response object
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Response: Response after all interceptors have been applied
|
|
211
|
+
|
|
212
|
+
Raises:
|
|
213
|
+
AtomHTTPRequestError: If any interceptor fails or returns invalid type
|
|
214
|
+
"""
|
|
215
|
+
for idx, interceptor in enumerate(self.interceptors.response_interceptors):
|
|
216
|
+
try:
|
|
217
|
+
# Execute interceptor (supports both sync and async functions)
|
|
218
|
+
result = interceptor(response)
|
|
219
|
+
if asyncio.iscoroutine(result):
|
|
220
|
+
response = await result
|
|
221
|
+
else:
|
|
222
|
+
response = result
|
|
223
|
+
|
|
224
|
+
# Validate interceptor return type
|
|
225
|
+
if not isinstance(response, Response):
|
|
226
|
+
raise AtomHTTPRequestError(
|
|
227
|
+
f"Response interceptor {idx} must return Response",
|
|
228
|
+
request=response.config,
|
|
229
|
+
config=response.config
|
|
230
|
+
)
|
|
231
|
+
except AtomHTTPError:
|
|
232
|
+
raise
|
|
233
|
+
except Exception as e:
|
|
234
|
+
# Wrap any interceptor exception in AtomHTTP error
|
|
235
|
+
raise AtomHTTPRequestError(
|
|
236
|
+
f"Response interceptor {idx} error: {str(e)}",
|
|
237
|
+
request=response.config,
|
|
238
|
+
config=response.config
|
|
239
|
+
)
|
|
240
|
+
return response
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Response Module
|
|
3
|
+
---------------
|
|
4
|
+
HTTP response object for AtomHTTP client.
|
|
5
|
+
|
|
6
|
+
This module provides the Response class that wraps HTTP responses with
|
|
7
|
+
a consistent interface similar to axios responses. It includes response
|
|
8
|
+
data, status code, headers, and references to the original request
|
|
9
|
+
configuration.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Response:
|
|
18
|
+
"""
|
|
19
|
+
HTTP response object similar to axios response interface.
|
|
20
|
+
|
|
21
|
+
This class encapsulates all components of an HTTP response including
|
|
22
|
+
the parsed body, status information, headers, and the original request
|
|
23
|
+
configuration. It provides a clean, consistent interface for accessing
|
|
24
|
+
response data.
|
|
25
|
+
|
|
26
|
+
The Response object is immutable by design (frozen dataclass semantics
|
|
27
|
+
are not enforced but should be treated as read-only after creation).
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
data (Any): Parsed response body. Type depends on responseType:
|
|
31
|
+
- 'json': dict or list
|
|
32
|
+
- 'text': str
|
|
33
|
+
- 'blob'/'arraybuffer': bytes
|
|
34
|
+
- 'stream': aiohttp.StreamReader
|
|
35
|
+
status (int): HTTP status code (e.g., 200, 404, 500)
|
|
36
|
+
status_text (str): HTTP status message (e.g., "OK", "Not Found")
|
|
37
|
+
headers (Dict[str, str]): Response headers as a case-insensitive dict
|
|
38
|
+
config (Any): Original RequestConfig object used for the request
|
|
39
|
+
request (Any): Reference to the original request configuration
|
|
40
|
+
(usually same as config, kept for axios compatibility)
|
|
41
|
+
|
|
42
|
+
Properties:
|
|
43
|
+
ok (bool): True if status code is in the 200-299 range
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
>>> response = Response(
|
|
47
|
+
... data={'id': 1, 'name': 'John'},
|
|
48
|
+
... status=200,
|
|
49
|
+
... status_text='OK',
|
|
50
|
+
... headers={'Content-Type': 'application/json'},
|
|
51
|
+
... config=original_config,
|
|
52
|
+
... request=original_config
|
|
53
|
+
... )
|
|
54
|
+
>>> print(response.ok)
|
|
55
|
+
True
|
|
56
|
+
>>> print(response.status)
|
|
57
|
+
200
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
data: Any
|
|
61
|
+
status: int
|
|
62
|
+
status_text: str
|
|
63
|
+
headers: Dict[str, str]
|
|
64
|
+
config: Any
|
|
65
|
+
request: Any
|
|
66
|
+
|
|
67
|
+
def __repr__(self) -> str:
|
|
68
|
+
"""
|
|
69
|
+
Return a string representation of the response.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
str: Formatted response string with status code and message
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
>>> response = Response(data={}, status=200, status_text='OK', ...)
|
|
76
|
+
>>> print(repr(response))
|
|
77
|
+
'<Response [200] OK>'
|
|
78
|
+
"""
|
|
79
|
+
return f"<Response [{self.status}] {self.status_text}>"
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def ok(self) -> bool:
|
|
83
|
+
"""
|
|
84
|
+
Check if the response status indicates success.
|
|
85
|
+
|
|
86
|
+
A response is considered "ok" when the HTTP status code falls within
|
|
87
|
+
the 200-299 range (successful responses).
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
bool: True if status code is between 200 and 299 inclusive,
|
|
91
|
+
False otherwise
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
>>> response = Response(data={}, status=200, status_text='OK', ...)
|
|
95
|
+
>>> response.ok
|
|
96
|
+
True
|
|
97
|
+
>>> response = Response(data={}, status=404, status_text='Not Found', ...)
|
|
98
|
+
>>> response.ok
|
|
99
|
+
False
|
|
100
|
+
"""
|
|
101
|
+
return 200 <= self.status < 300
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP Error Module
|
|
3
|
+
-----------------
|
|
4
|
+
Custom exception classes for AtomHTTP client error handling.
|
|
5
|
+
|
|
6
|
+
This module provides a hierarchy of exception classes for different types
|
|
7
|
+
of HTTP request failures, similar to axios error handling. Each error type
|
|
8
|
+
includes relevant context such as request configuration, response data,
|
|
9
|
+
and error codes for programmatic error identification.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Optional, Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AtomHTTPError(Exception):
|
|
16
|
+
"""
|
|
17
|
+
Base exception class for all AtomHTTP errors.
|
|
18
|
+
|
|
19
|
+
This is the parent class for all AtomHTTP-specific exceptions. It provides
|
|
20
|
+
common attributes that are useful for debugging and error handling.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
message (str): Human-readable error description
|
|
24
|
+
config (Optional[Any]): Request configuration that caused the error
|
|
25
|
+
response (Optional[Any]): Response object if error occurred after receiving response
|
|
26
|
+
request (Optional[Any]): Original request configuration (alias for config)
|
|
27
|
+
code (Optional[str]): Standardized error code for programmatic handling
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
message: str,
|
|
33
|
+
config: Optional[Any] = None,
|
|
34
|
+
response: Optional[Any] = None,
|
|
35
|
+
request: Optional[Any] = None
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Initialize a AtomHTTP error with optional context.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
message (str): Human-readable error description
|
|
42
|
+
config (Optional[Any]): Request configuration used for the request
|
|
43
|
+
response (Optional[Any]): Response object (if available)
|
|
44
|
+
request (Optional[Any]): Original request (usually same as config)
|
|
45
|
+
"""
|
|
46
|
+
super().__init__(message)
|
|
47
|
+
self.message = message
|
|
48
|
+
self.config = config
|
|
49
|
+
self.response = response
|
|
50
|
+
self.request = request
|
|
51
|
+
self.code: Optional[str] = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AtomHTTPRequestError(AtomHTTPError):
|
|
55
|
+
"""
|
|
56
|
+
Exception raised for bad requests or client errors (4xx status codes).
|
|
57
|
+
|
|
58
|
+
This error occurs when the request is malformed, validation fails,
|
|
59
|
+
or the server responds with a client error status code (400-499).
|
|
60
|
+
|
|
61
|
+
Error Code: 'ERR_BAD_REQUEST'
|
|
62
|
+
|
|
63
|
+
Attributes:
|
|
64
|
+
code (str): Standardized error code always set to 'ERR_BAD_REQUEST'
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
message: str,
|
|
70
|
+
request: Optional[Any] = None,
|
|
71
|
+
config: Optional[Any] = None,
|
|
72
|
+
response: Optional[Any] = None
|
|
73
|
+
):
|
|
74
|
+
"""
|
|
75
|
+
Initialize a request error.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
message (str): Error description
|
|
79
|
+
request (Optional[Any]): Original request that failed
|
|
80
|
+
config (Optional[Any]): Request configuration (alias for request)
|
|
81
|
+
response (Optional[Any]): Response from server (if available)
|
|
82
|
+
"""
|
|
83
|
+
super().__init__(message, config=config, response=response, request=request)
|
|
84
|
+
self.code = 'ERR_BAD_REQUEST'
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class AtomHTTPNetworkError(AtomHTTPError):
|
|
88
|
+
"""
|
|
89
|
+
Exception raised for network-related failures.
|
|
90
|
+
|
|
91
|
+
This error occurs when the request cannot reach the server due to
|
|
92
|
+
network issues such as DNS resolution failure, connection refused,
|
|
93
|
+
SSL errors, or general connectivity problems.
|
|
94
|
+
|
|
95
|
+
Error Code: 'ERR_NETWORK'
|
|
96
|
+
|
|
97
|
+
Attributes:
|
|
98
|
+
code (str): Standardized error code always set to 'ERR_NETWORK'
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(self, message: str, config: Optional[Any] = None):
|
|
102
|
+
"""
|
|
103
|
+
Initialize a network error.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
message (str): Error description (connection error, DNS error, etc.)
|
|
107
|
+
config (Optional[Any]): Request configuration used for the request
|
|
108
|
+
"""
|
|
109
|
+
super().__init__(message, config=config)
|
|
110
|
+
self.code = 'ERR_NETWORK'
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class AtomHTTPTimeoutError(AtomHTTPError):
|
|
114
|
+
"""
|
|
115
|
+
Exception raised when a request exceeds the configured timeout.
|
|
116
|
+
|
|
117
|
+
This error occurs when the request takes longer than the specified
|
|
118
|
+
timeout value. The request may have been partially sent or the
|
|
119
|
+
response may have been partially received before timeout occurred.
|
|
120
|
+
|
|
121
|
+
Error Code: 'ECONNABORTED'
|
|
122
|
+
|
|
123
|
+
Attributes:
|
|
124
|
+
code (str): Standardized error code always set to 'ECONNABORTED'
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
message: str,
|
|
130
|
+
config: Optional[Any] = None,
|
|
131
|
+
request: Optional[Any] = None
|
|
132
|
+
):
|
|
133
|
+
"""
|
|
134
|
+
Initialize a timeout error.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
message (str): Error description with timeout duration
|
|
138
|
+
config (Optional[Any]): Request configuration used for the request
|
|
139
|
+
request (Optional[Any]): Original request (alias for config)
|
|
140
|
+
"""
|
|
141
|
+
super().__init__(message, config=config, request=request)
|
|
142
|
+
self.code = 'ECONNABORTED'
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interceptor Manager Module
|
|
3
|
+
--------------------------
|
|
4
|
+
Manages request and response interceptors for the AtomHTTP client.
|
|
5
|
+
|
|
6
|
+
This module provides the InterceptorManager class which handles registration,
|
|
7
|
+
execution order, and removal of interceptors. Interceptors allow modifying
|
|
8
|
+
requests before they are sent and responses before they are returned to
|
|
9
|
+
the caller, similar to middleware in other HTTP clients.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import List, Callable, Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InterceptorManager:
|
|
16
|
+
"""
|
|
17
|
+
Manages request and response interceptors for the HTTP client.
|
|
18
|
+
|
|
19
|
+
Interceptors are functions that can modify requests before they are
|
|
20
|
+
sent or modify responses before they are returned to the caller.
|
|
21
|
+
This enables functionality such as:
|
|
22
|
+
- Adding authentication headers to all requests
|
|
23
|
+
- Logging requests and responses
|
|
24
|
+
- Retrying failed requests
|
|
25
|
+
- Transforming request/response data
|
|
26
|
+
- Handling errors globally
|
|
27
|
+
|
|
28
|
+
The interceptor execution order follows the registration order:
|
|
29
|
+
- Request interceptors execute in the order they were added
|
|
30
|
+
- Response interceptors execute in the order they were added
|
|
31
|
+
|
|
32
|
+
Interceptors can be either synchronous or asynchronous functions.
|
|
33
|
+
Asynchronous interceptors must be declared with `async def`.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
request_interceptors (List[Callable]): List of request interceptor functions
|
|
37
|
+
response_interceptors (List[Callable]): List of response interceptor functions
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
"""
|
|
42
|
+
Initialize an empty interceptor manager.
|
|
43
|
+
|
|
44
|
+
Both request and response interceptor lists start empty.
|
|
45
|
+
Use the use() method to add interceptors.
|
|
46
|
+
"""
|
|
47
|
+
self.request_interceptors: List[Callable] = []
|
|
48
|
+
self.response_interceptors: List[Callable] = []
|
|
49
|
+
|
|
50
|
+
def use(self, interceptor: Callable, is_response: bool = False) -> int:
|
|
51
|
+
"""
|
|
52
|
+
Add an interceptor to the manager.
|
|
53
|
+
|
|
54
|
+
The interceptor will be added to the end of the list and will
|
|
55
|
+
be executed after all previously added interceptors.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
interceptor (Callable): The interceptor function.
|
|
59
|
+
Can be sync: `fn(config)` or `fn(response)`
|
|
60
|
+
or async: `async fn(config)` or `async fn(response)`
|
|
61
|
+
is_response (bool): If True, adds to response interceptors list.
|
|
62
|
+
If False (default), adds to request interceptors list.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
int: The index of the added interceptor. This index can be used
|
|
66
|
+
with the eject() method to remove the interceptor later.
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
>>> manager = InterceptorManager()
|
|
70
|
+
>>>
|
|
71
|
+
>>> # Request interceptor (adds auth header)
|
|
72
|
+
>>> async def add_auth(config):
|
|
73
|
+
... config.headers['Authorization'] = 'Bearer token'
|
|
74
|
+
... return config
|
|
75
|
+
>>>
|
|
76
|
+
>>> index = manager.use(add_auth) # Request interceptor
|
|
77
|
+
>>>
|
|
78
|
+
>>> # Response interceptor (logs response)
|
|
79
|
+
>>> async def log_response(response):
|
|
80
|
+
... print(f"Status: {response.status}")
|
|
81
|
+
... return response
|
|
82
|
+
>>>
|
|
83
|
+
>>> resp_index = manager.use(log_response, is_response=True)
|
|
84
|
+
"""
|
|
85
|
+
if is_response:
|
|
86
|
+
self.response_interceptors.append(interceptor)
|
|
87
|
+
return len(self.response_interceptors) - 1
|
|
88
|
+
else:
|
|
89
|
+
self.request_interceptors.append(interceptor)
|
|
90
|
+
return len(self.request_interceptors) - 1
|
|
91
|
+
|
|
92
|
+
def eject(self, index: int, is_response: bool = False) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Remove an interceptor by its index.
|
|
95
|
+
|
|
96
|
+
This method removes the interceptor at the specified index from
|
|
97
|
+
either the request or response interceptor list. The index must
|
|
98
|
+
be the one returned by the use() method when the interceptor
|
|
99
|
+
was added.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
index (int): Index of the interceptor to remove
|
|
103
|
+
is_response (bool): If True, removes from response interceptors list.
|
|
104
|
+
If False (default), removes from request interceptors list.
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
>>> manager = InterceptorManager()
|
|
108
|
+
>>> index = manager.use(some_interceptor)
|
|
109
|
+
>>> # Later...
|
|
110
|
+
>>> manager.eject(index) # Remove the interceptor
|
|
111
|
+
|
|
112
|
+
Note:
|
|
113
|
+
If the index is out of range, the method does nothing (no error raised).
|
|
114
|
+
"""
|
|
115
|
+
if is_response:
|
|
116
|
+
if 0 <= index < len(self.response_interceptors):
|
|
117
|
+
self.response_interceptors.pop(index)
|
|
118
|
+
else:
|
|
119
|
+
if 0 <= index < len(self.request_interceptors):
|
|
120
|
+
self.request_interceptors.pop(index)
|
|
121
|
+
|
|
122
|
+
def clear(self) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Remove all registered interceptors.
|
|
125
|
+
|
|
126
|
+
This method clears both request and response interceptor lists,
|
|
127
|
+
effectively resetting the manager to its initial empty state.
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
>>> manager = InterceptorManager()
|
|
131
|
+
>>> manager.use(interceptor1)
|
|
132
|
+
>>> manager.use(interceptor2, is_response=True)
|
|
133
|
+
>>> manager.clear() # Removes all interceptors
|
|
134
|
+
"""
|
|
135
|
+
self.request_interceptors.clear()
|
|
136
|
+
self.response_interceptors.clear()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Request Interceptor Type Definition
|
|
3
|
+
-----------------------------------
|
|
4
|
+
Type hint for request interceptor functions in AtomHTTP.
|
|
5
|
+
|
|
6
|
+
This module defines the RequestInterceptor type alias for use in type annotations
|
|
7
|
+
throughout the AtomHTTP codebase. Request interceptors are functions that
|
|
8
|
+
modify or inspect request configurations before they are sent.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Callable, Awaitable
|
|
12
|
+
from ..core.config import RequestConfig
|
|
13
|
+
|
|
14
|
+
# Type alias for request interceptor functions
|
|
15
|
+
# Request interceptors receive a RequestConfig object and return a
|
|
16
|
+
# (potentially modified) RequestConfig object. They can be either
|
|
17
|
+
# synchronous or asynchronous.
|
|
18
|
+
RequestInterceptor = Callable[[RequestConfig], Awaitable[RequestConfig]]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Response Interceptor Type Definition
|
|
3
|
+
------------------------------------
|
|
4
|
+
Type hint for response interceptor functions in AtomHTTP.
|
|
5
|
+
|
|
6
|
+
This module defines the ResponseInterceptor type alias for use in type annotations
|
|
7
|
+
throughout the AtomHTTP codebase. Response interceptors are functions that
|
|
8
|
+
modify or inspect response objects before they are returned to the caller.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Callable, Awaitable
|
|
12
|
+
from ..core.response import Response
|
|
13
|
+
|
|
14
|
+
# Type alias for response interceptor functions
|
|
15
|
+
# Response interceptors receive a Response object and return a
|
|
16
|
+
# (potentially modified) Response object. They can be either
|
|
17
|
+
# synchronous or asynchronous.
|
|
18
|
+
ResponseInterceptor = Callable[[Response], Awaitable[Response]]
|