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.
@@ -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,13 @@
1
+ from .http_errors import (
2
+ AtomHTTPError,
3
+ AtomHTTPRequestError,
4
+ AtomHTTPNetworkError,
5
+ AtomHTTPTimeoutError
6
+ )
7
+
8
+ __all__ = [
9
+ 'AtomHTTPError',
10
+ 'AtomHTTPRequestError',
11
+ 'AtomHTTPNetworkError',
12
+ 'AtomHTTPTimeoutError'
13
+ ]
@@ -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,5 @@
1
+ from .manager import InterceptorManager
2
+ from .request_interceptor import RequestInterceptor
3
+ from .response_interceptor import ResponseInterceptor
4
+
5
+ __all__ = ['InterceptorManager', 'RequestInterceptor', 'ResponseInterceptor']
@@ -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]]
@@ -0,0 +1,3 @@
1
+ from .upload_progress import ProgressTracker
2
+
3
+ __all__ = ['ProgressTracker']