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,186 @@
1
+ """
2
+ Request Configuration Module
3
+ ----------------------------
4
+ Defines the configuration data class for HTTP requests in AtomHTTP.
5
+
6
+ This module provides the RequestConfig dataclass which holds all configuration
7
+ parameters for an HTTP request, similar to axios configuration objects.
8
+ It supports timeouts, headers, authentication, proxies, redirects, data
9
+ transformation, and various other request settings.
10
+ """
11
+
12
+ from dataclasses import dataclass, field, asdict
13
+ from typing import Dict, Any, Optional, Callable, Union
14
+ from datetime import timedelta
15
+
16
+
17
+ @dataclass
18
+ class RequestConfig:
19
+ """
20
+ Configuration object for HTTP requests, inspired by axios config.
21
+
22
+ This dataclass holds all parameters needed to configure and execute
23
+ an HTTP request. It provides a flexible and extensible way to specify
24
+ request options including URL, method, headers, data, timeouts,
25
+ authentication, proxy settings, and response handling.
26
+
27
+ Attributes:
28
+ url (str): Target URL for the request. Can be absolute or relative
29
+ when used with baseURL.
30
+
31
+ method (str): HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS).
32
+ Defaults to 'GET'.
33
+
34
+ baseURL (str): Base URL prepended to relative URLs. Useful for API clients.
35
+
36
+ headers (Dict[str, str]): Custom HTTP headers to send with the request.
37
+ These override default headers.
38
+
39
+ params (Dict[str, Any]): Query parameters to append to the URL.
40
+ Automatically URL-encoded.
41
+
42
+ data (Any): Request body data. Can be dict (JSON), str, bytes,
43
+ FormData, or file-like object.
44
+
45
+ timeout (Union[int, float, timedelta]): Request timeout in seconds.
46
+ Can be integer, float, or timedelta.
47
+ Defaults to 30 seconds.
48
+
49
+ withCredentials (bool): Whether to send cookies with cross-site requests.
50
+ Defaults to False.
51
+
52
+ auth (Optional[Dict[str, str]]): Basic authentication credentials.
53
+ Format: {'username': 'user', 'password': 'pass'}
54
+
55
+ proxy (Optional[Dict[str, Any]]): Proxy server configuration.
56
+ Format: {'host': 'http://proxy:8080', 'auth': {...}}
57
+
58
+ maxRedirects (int): Maximum number of redirects to follow. Defaults to 5.
59
+
60
+ maxContentLength (int): Maximum allowed response body size in bytes.
61
+ -1 means no limit. Defaults to -1.
62
+
63
+ maxBodyLength (int): Maximum allowed request body size in bytes.
64
+ -1 means no limit. Defaults to -1.
65
+
66
+ transformRequest (Optional[Callable]): Function to transform request data
67
+ before sending.
68
+
69
+ transformResponse (Optional[Callable]): Function to transform response data
70
+ before returning.
71
+
72
+ responseType (str): Expected response type. Options: 'json', 'text',
73
+ 'blob', 'arraybuffer', 'stream'. Defaults to 'json'.
74
+
75
+ xsrfCookieName (str): Name of the cookie containing the XSRF token.
76
+ Defaults to 'XSRF-TOKEN'.
77
+
78
+ xsrfHeaderName (str): Name of the header to send XSRF token in.
79
+ Defaults to 'X-XSRF-TOKEN'.
80
+
81
+ onUploadProgress (Optional[Callable]): Callback for upload progress.
82
+ Receives (loaded, total) arguments.
83
+
84
+ onDownloadProgress (Optional[Callable]): Callback for download progress.
85
+ Receives (loaded, total) arguments.
86
+
87
+ socketPath (Optional[str]): Unix domain socket path for connection.
88
+ Example: '/var/run/docker.sock'
89
+
90
+ keepAlive (bool): Enable HTTP keep-alive connections. Defaults to True.
91
+
92
+ decompress (bool): Automatically decompress gzip/deflate responses.
93
+ Defaults to True.
94
+
95
+ validateStatus (Optional[Callable]): Function to determine if status code
96
+ should resolve or reject. Receives status.
97
+
98
+ adapter (Optional[Any]): Custom HTTP adapter for request execution.
99
+
100
+ transitional (Dict[str, bool]): Transitional configuration options for
101
+ backward compatibility.
102
+ """
103
+
104
+ # Core request parameters
105
+ url: str = ''
106
+ method: str = 'GET'
107
+ baseURL: str = ''
108
+
109
+ # Headers and parameters
110
+ headers: Dict[str, str] = field(default_factory=dict)
111
+ params: Dict[str, Any] = field(default_factory=dict)
112
+ data: Any = None
113
+
114
+ # Timing and connection settings
115
+ timeout: Union[int, float, timedelta] = 30
116
+ withCredentials: bool = False
117
+
118
+ # Authentication and proxy
119
+ auth: Optional[Dict[str, str]] = None
120
+ proxy: Optional[Dict[str, Any]] = None
121
+
122
+ # Request limits and redirects
123
+ maxRedirects: int = 5
124
+ maxContentLength: int = -1
125
+ maxBodyLength: int = -1
126
+
127
+ # Data transformation
128
+ transformRequest: Optional[Callable] = None
129
+ transformResponse: Optional[Callable] = None
130
+
131
+ # Response handling
132
+ responseType: str = 'json'
133
+
134
+ # CSRF protection
135
+ xsrfCookieName: str = 'XSRF-TOKEN'
136
+ xsrfHeaderName: str = 'X-XSRF-TOKEN'
137
+
138
+ # Progress tracking
139
+ onUploadProgress: Optional[Callable] = None
140
+ onDownloadProgress: Optional[Callable] = None
141
+
142
+ # Low-level connection options
143
+ socketPath: Optional[str] = None
144
+ keepAlive: bool = True
145
+ decompress: bool = True
146
+
147
+ # Status validation
148
+ validateStatus: Optional[Callable] = None
149
+
150
+ # Custom adapter
151
+ adapter: Optional[Any] = None
152
+
153
+ # Transitional options for backward compatibility
154
+ transitional: Dict[str, bool] = field(default_factory=lambda: {
155
+ 'silentJSONParsing': True,
156
+ 'forcedJSONParsing': True,
157
+ 'clarifyTimeoutError': False
158
+ })
159
+
160
+ def to_dict(self) -> Dict[str, Any]:
161
+ """
162
+ Convert the configuration to a dictionary, omitting None values.
163
+
164
+ This method serializes the RequestConfig object to a dictionary
165
+ suitable for JSON serialization or for passing to other functions.
166
+ Special handling is applied to timedelta timeout values, converting
167
+ them to seconds.
168
+
169
+ Returns:
170
+ Dict[str, Any]: Dictionary representation of the config with all
171
+ non-None values included.
172
+
173
+ Example:
174
+ >>> config = RequestConfig(url='/api', timeout=timedelta(seconds=5))
175
+ >>> config.to_dict()
176
+ {'url': '/api', 'timeout': 5.0}
177
+ """
178
+ # Convert dataclass to dictionary
179
+ data = asdict(self)
180
+
181
+ # Convert timedelta to seconds if present
182
+ if isinstance(self.timeout, timedelta):
183
+ data['timeout'] = self.timeout.total_seconds()
184
+
185
+ # Remove None values to keep the dictionary clean
186
+ return {k: v for k, v in data.items() if v is not None}
@@ -0,0 +1,212 @@
1
+ """
2
+ Defaults Configuration Module
3
+ -----------------------------
4
+ Manages default configuration settings for the AtomHTTP client.
5
+
6
+ This module provides the Defaults class which maintains the default
7
+ configuration values for all AtomHTTP client instances. It supports
8
+ merging custom configurations with defaults, updating individual
9
+ settings, and accessing default values as attributes.
10
+ """
11
+
12
+ from typing import Dict, Any
13
+ from .config import RequestConfig
14
+
15
+
16
+ class Defaults:
17
+ """
18
+ Default configuration manager for AtomHTTP client instances.
19
+
20
+ This class holds the default configuration that will be applied to
21
+ all requests made by a AtomHTTP client unless overridden. It supports
22
+ updating defaults with custom values, accessing defaults as attributes,
23
+ and converting defaults to dictionary format.
24
+
25
+ The Defaults class is designed to be used internally by the AtomHTTP
26
+ client, but can also be accessed directly for advanced use cases.
27
+
28
+ Features:
29
+ - Centralized default configuration management
30
+ - Deep merging of header dictionaries
31
+ - Attribute-style access to configuration values
32
+ - Support for updating individual settings
33
+ - Dictionary conversion for serialization
34
+
35
+ Attributes:
36
+ _config (RequestConfig): Internal RequestConfig object holding all
37
+ default values.
38
+
39
+ Example:
40
+ >>> defaults = Defaults()
41
+ >>> print(defaults.timeout)
42
+ 30
43
+ >>> defaults.timeout = 60
44
+ >>> print(defaults.timeout)
45
+ 60
46
+ >>> defaults.update(RequestConfig(headers={'X-Custom': 'value'}))
47
+ >>> print(defaults.headers['X-Custom'])
48
+ 'value'
49
+ """
50
+
51
+ def __init__(self):
52
+ """
53
+ Initialize default configuration with sensible defaults.
54
+
55
+ Default values are chosen to provide a balanced experience
56
+ between security, performance, and compatibility:
57
+ - Accept header accepts JSON, text, and any format
58
+ - User-Agent identifies the client as atomhttp/2.0.0
59
+ - 30 second timeout prevents hanging requests
60
+ - Up to 5 redirects followed automatically
61
+ - JSON response parsing by default
62
+ - CSRF protection enabled with standard header names
63
+ - Credentials not sent cross-origin by default
64
+ - Keep-alive enabled for connection reuse
65
+ - Automatic gzip/deflate decompression enabled
66
+ """
67
+ self._config = RequestConfig(
68
+ # Default headers for all requests
69
+ headers={
70
+ 'Accept': 'application/json, text/plain, */*',
71
+ 'User-Agent': 'atomhttp/2.0.0'
72
+ },
73
+ # Request timeout in seconds
74
+ timeout=30,
75
+ # Maximum number of redirects to follow
76
+ maxRedirects=5,
77
+ # Default response parsing type
78
+ responseType='json',
79
+ # CSRF protection cookie name
80
+ xsrfCookieName='XSRF-TOKEN',
81
+ # CSRF protection header name
82
+ xsrfHeaderName='X-XSRF-TOKEN',
83
+ # Do not send credentials cross-origin by default
84
+ withCredentials=False,
85
+ # Enable HTTP keep-alive for connection reuse
86
+ keepAlive=True,
87
+ # Automatically decompress compressed responses
88
+ decompress=True,
89
+ # No base URL by default (use absolute URLs)
90
+ baseURL=''
91
+ )
92
+
93
+ def update(self, config: RequestConfig):
94
+ """
95
+ Update default configuration with values from another config.
96
+
97
+ This method merges the provided configuration into the defaults.
98
+ Headers are merged (custom headers added, existing ones overwritten),
99
+ while other fields are replaced entirely if non-None.
100
+
101
+ Args:
102
+ config (RequestConfig): Configuration containing values to merge
103
+ into the defaults. Only non-None values
104
+ are considered for the update.
105
+
106
+ Example:
107
+ >>> defaults = Defaults()
108
+ >>> custom = RequestConfig(
109
+ ... headers={'X-API-Key': '12345'},
110
+ ... timeout=15
111
+ ... )
112
+ >>> defaults.update(custom)
113
+ >>> print(defaults.timeout) # Updated to 15
114
+ >>> print(defaults.headers['X-API-Key']) # New header added
115
+ """
116
+ # Convert config to dictionary for iteration
117
+ config_dict = config.to_dict()
118
+
119
+ # Process each key-value pair
120
+ for key, value in config_dict.items():
121
+ # Skip None values (they represent no change)
122
+ if value is not None and hasattr(self._config, key):
123
+ # Special handling for headers: merge dictionaries instead of replacing
124
+ if key == 'headers' and isinstance(value, dict):
125
+ self._config.headers.update(value)
126
+ else:
127
+ # Replace other attributes directly
128
+ setattr(self._config, key, value)
129
+
130
+ def to_dict(self) -> Dict[str, Any]:
131
+ """
132
+ Convert all default configuration to a dictionary.
133
+
134
+ This method returns a complete dictionary representation of the
135
+ current default configuration, with None values omitted.
136
+
137
+ Returns:
138
+ Dict[str, Any]: Dictionary containing all non-None default
139
+ configuration values.
140
+
141
+ Example:
142
+ >>> defaults = Defaults()
143
+ >>> defaults_dict = defaults.to_dict()
144
+ >>> print(defaults_dict.keys())
145
+ dict_keys(['headers', 'timeout', 'maxRedirects', ...])
146
+ """
147
+ return self._config.to_dict()
148
+
149
+ def __getattr__(self, name):
150
+ """
151
+ Access configuration values as attributes.
152
+
153
+ This magic method allows direct attribute access to configuration
154
+ values stored in the internal _config object.
155
+
156
+ Args:
157
+ name (str): Name of the configuration attribute to retrieve
158
+
159
+ Returns:
160
+ Any: Value of the requested configuration attribute
161
+
162
+ Raises:
163
+ AttributeError: If the attribute doesn't exist in _config
164
+
165
+ Example:
166
+ >>> defaults = Defaults()
167
+ >>> print(defaults.timeout) # Access via __getattr__
168
+ 30
169
+ """
170
+ return getattr(self._config, name)
171
+
172
+ def __setattr__(self, name, value):
173
+ """
174
+ Set configuration values as attributes.
175
+
176
+ This magic method routes attribute assignment to the internal
177
+ _config object, except for the special '_config' attribute itself.
178
+
179
+ Args:
180
+ name (str): Name of the configuration attribute to set
181
+ value (Any): Value to assign to the configuration attribute
182
+
183
+ Example:
184
+ >>> defaults = Defaults()
185
+ >>> defaults.timeout = 45 # Sets _config.timeout = 45
186
+ """
187
+ # Special handling for the internal _config attribute
188
+ if name == '_config':
189
+ super().__setattr__(name, value)
190
+ else:
191
+ # Route all other attribute assignments to the _config object
192
+ setattr(self._config, name, value)
193
+
194
+ @property
195
+ def baseURL(self) -> str:
196
+ """
197
+ Get the base URL configured in defaults.
198
+
199
+ Returns:
200
+ str: Current base URL value (empty string if not set)
201
+ """
202
+ return self._config.baseURL
203
+
204
+ @baseURL.setter
205
+ def baseURL(self, value: str):
206
+ """
207
+ Set the base URL in defaults.
208
+
209
+ Args:
210
+ value (str): New base URL value to set
211
+ """
212
+ self._config.baseURL = value
@@ -0,0 +1,282 @@
1
+ """
2
+ FormData Module
3
+ ---------------
4
+ Provides multipart/form-data and URL-encoded form data handling for AtomHTTP.
5
+
6
+ This module implements a FormData class that mimics the browser's FormData API,
7
+ allowing easy construction of form data for HTTP requests. It supports:
8
+ - Multiple values for the same field name
9
+ - File uploads with automatic MIME type detection
10
+ - Mixed field and file data in the same form
11
+ - Streaming of file contents without loading entire files into memory
12
+ - Conversion to both multipart/form-data and application/x-www-form-urlencoded formats
13
+ """
14
+
15
+ import io
16
+ import os
17
+ import random
18
+ import string
19
+ from typing import Dict, Any, Optional, Union, Tuple, List
20
+ from pathlib import Path
21
+ from urllib.parse import urlencode
22
+
23
+
24
+ class FormDataItem:
25
+ """
26
+ Represents a single item in a FormData field.
27
+
28
+ FormData fields can have multiple values (e.g., for multi-select inputs),
29
+ and each value is stored as a FormDataItem. This class holds the value
30
+ along with optional metadata for file uploads.
31
+
32
+ Attributes:
33
+ value (Any): The actual data value (string, bytes, file-like, Path, etc.)
34
+ filename (Optional[str]): Original filename for file uploads
35
+ content_type (Optional[str]): MIME type of the file content
36
+ """
37
+
38
+ def __init__(self, value: Any, filename: Optional[str] = None, content_type: Optional[str] = None):
39
+ """
40
+ Initialize a form data item.
41
+
42
+ Args:
43
+ value (Any): The data value. Can be string, bytes, file object, Path, etc.
44
+ filename (Optional[str]): Original filename (for file uploads)
45
+ content_type (Optional[str]): MIME type (auto-detected if not provided)
46
+ """
47
+ self.value = value
48
+ self.filename = filename
49
+ self.content_type = content_type
50
+
51
+
52
+ class FormData:
53
+ """
54
+ FormData implementation similar to browser FormData API.
55
+
56
+ This class provides a convenient interface for constructing form data
57
+ suitable for HTTP requests, supporting both multipart/form-data (for
58
+ file uploads and mixed content) and URL-encoded (for simple forms).
59
+
60
+ Features:
61
+ - Append multiple values to the same field name
62
+ - Automatic MIME type detection from file extensions
63
+ - Support for various value types (str, bytes, file objects, Path)
64
+ - Streaming of large file contents
65
+ - Boundary generation for multipart requests
66
+ """
67
+
68
+ def __init__(self):
69
+ """
70
+ Initialize an empty FormData object.
71
+
72
+ The internal data structure maps field names to lists of FormDataItem
73
+ objects, allowing multiple values per field.
74
+ """
75
+ self._data: Dict[str, List[FormDataItem]] = {}
76
+ self._boundary: Optional[str] = None
77
+
78
+ def append(self, name: str, value: Any, filename: Optional[str] = None, content_type: Optional[str] = None):
79
+ """
80
+ Append a value to a form data field.
81
+
82
+ If the field name already exists, the new value is added to the list
83
+ of values for that field (multiple values are allowed).
84
+
85
+ Args:
86
+ name (str): Field name
87
+ value (Any): Field value (string, bytes, file object, Path, etc.)
88
+ filename (Optional[str]): Original filename (for file uploads)
89
+ content_type (Optional[str]): MIME type (auto-detected from filename if not provided)
90
+ """
91
+ if name not in self._data:
92
+ self._data[name] = []
93
+
94
+ item = FormDataItem(value, filename, content_type)
95
+ self._data[name].append(item)
96
+
97
+ def delete(self, name: str):
98
+ """
99
+ Delete all values for a form data field.
100
+
101
+ Args:
102
+ name (str): Field name to remove
103
+ """
104
+ if name in self._data:
105
+ del self._data[name]
106
+
107
+ def get(self, name: str) -> Optional[Any]:
108
+ """
109
+ Get the first value of a form data field.
110
+
111
+ Args:
112
+ name (str): Field name to retrieve
113
+
114
+ Returns:
115
+ Optional[Any]: First value of the field, or None if field doesn't exist
116
+ """
117
+ if name in self._data and self._data[name]:
118
+ return self._data[name][0].value
119
+ return None
120
+
121
+ def get_all(self, name: str) -> List[Any]:
122
+ """
123
+ Get all values of a form data field.
124
+
125
+ Args:
126
+ name (str): Field name to retrieve
127
+
128
+ Returns:
129
+ List[Any]: List of all values for the field (empty list if field doesn't exist)
130
+ """
131
+ if name in self._data:
132
+ return [item.value for item in self._data[name]]
133
+ return []
134
+
135
+ def has(self, name: str) -> bool:
136
+ """
137
+ Check if a field exists in the form data.
138
+
139
+ Args:
140
+ name (str): Field name to check
141
+
142
+ Returns:
143
+ bool: True if field exists, False otherwise
144
+ """
145
+ return name in self._data
146
+
147
+ def keys(self) -> List[str]:
148
+ """
149
+ Get all field names in the form data.
150
+
151
+ Returns:
152
+ List[str]: List of all field names
153
+ """
154
+ return list(self._data.keys())
155
+
156
+ def values(self) -> List[Any]:
157
+ """
158
+ Get all values in the form data (flattened).
159
+
160
+ Returns:
161
+ List[Any]: List of all values (all items from all fields)
162
+ """
163
+ return [item.value for items in self._data.values() for item in items]
164
+
165
+ def items(self) -> List[Tuple[str, Any]]:
166
+ """
167
+ Get all field-value pairs (flattened).
168
+
169
+ Returns:
170
+ List[Tuple[str, Any]]: List of (field_name, value) tuples
171
+ """
172
+ return [(name, item.value) for name, items in self._data.items() for item in items]
173
+
174
+ def _generate_boundary(self) -> str:
175
+ """
176
+ Generate a unique multipart boundary string.
177
+
178
+ The boundary is a random string that separates different parts of
179
+ a multipart/form-data request. It's designed to be unique enough
180
+ to not appear in the actual data.
181
+
182
+ Returns:
183
+ str: Random boundary string
184
+ """
185
+ return '----WebKitFormBoundary' + ''.join(random.choices(string.ascii_letters + string.digits, k=16))
186
+
187
+ def to_multipart(self) -> Tuple[bytes, str]:
188
+ """
189
+ Convert form data to multipart/form-data format.
190
+
191
+ This method serializes the FormData object into the multipart format
192
+ required by HTTP requests. Each field becomes a separate part with
193
+ its own headers and content.
194
+
195
+ The format follows RFC 7578 (Returning Values from Forms: multipart/form-data).
196
+
197
+ Returns:
198
+ Tuple[bytes, str]: A tuple containing:
199
+ - bytes: The complete multipart/form-data body
200
+ - str: The boundary string used for the multipart parts
201
+ """
202
+ if not self._boundary:
203
+ self._boundary = self._generate_boundary()
204
+
205
+ lines = []
206
+
207
+ for name, items in self._data.items():
208
+ for item in items:
209
+ lines.append(f'--{self._boundary}')
210
+ lines.append(f'Content-Disposition: form-data; name="{name}"')
211
+
212
+ if item.filename:
213
+ lines[-1] = f'Content-Disposition: form-data; name="{name}"; filename="{item.filename}"'
214
+
215
+ if item.content_type:
216
+ lines.append(f'Content-Type: {item.content_type}')
217
+ else:
218
+ ext = os.path.splitext(item.filename)[1].lower()
219
+ content_types = {
220
+ '.txt': 'text/plain',
221
+ '.json': 'application/json',
222
+ '.xml': 'application/xml',
223
+ '.html': 'text/html',
224
+ '.css': 'text/css',
225
+ '.js': 'application/javascript',
226
+ '.png': 'image/png',
227
+ '.jpg': 'image/jpeg',
228
+ '.jpeg': 'image/jpeg',
229
+ '.gif': 'image/gif',
230
+ '.pdf': 'application/pdf',
231
+ '.zip': 'application/zip',
232
+ '.mp4': 'video/mp4',
233
+ '.mp3': 'audio/mpeg',
234
+ }
235
+ lines.append(f'Content-Type: {content_types.get(ext, "application/octet-stream")}')
236
+
237
+ lines.append('')
238
+
239
+ if isinstance(item.value, (bytes, bytearray)):
240
+ lines.append(item.value)
241
+ elif isinstance(item.value, (io.IOBase, io.BufferedReader)):
242
+ lines.append(item.value.read())
243
+ elif isinstance(item.value, Path):
244
+ with open(item.value, 'rb') as f:
245
+ lines.append(f.read())
246
+ else:
247
+ lines.append(str(item.value).encode('utf-8'))
248
+
249
+ lines.append('')
250
+
251
+ lines.append(f'--{self._boundary}--')
252
+ lines.append('')
253
+
254
+ result = []
255
+ for line in lines:
256
+ if isinstance(line, bytes):
257
+ result.append(line)
258
+ else:
259
+ result.append(line.encode('utf-8'))
260
+ result.append(b'\r\n')
261
+
262
+ return b''.join(result), self._boundary
263
+
264
+ def to_urlencoded(self) -> str:
265
+ """
266
+ Convert form data to application/x-www-form-urlencoded format.
267
+
268
+ This method serializes the FormData object into the URL-encoded
269
+ format suitable for HTML forms without file uploads.
270
+
271
+ Note: Only the first value of each field is included in the output.
272
+ Multiple values are not supported in standard URL-encoded forms.
273
+
274
+ Returns:
275
+ str: URL-encoded form data string
276
+ """
277
+ params = {}
278
+ for name, items in self._data.items():
279
+ if items:
280
+ params[name] = items[0].value
281
+
282
+ return urlencode(params)