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,89 @@
1
+ """
2
+ Progress Tracker Module
3
+ -----------------------
4
+ Provides progress tracking functionality for upload and download operations.
5
+
6
+ This module contains the ProgressTracker class which monitors data transfer
7
+ progress and notifies callbacks as data is transmitted. It is used by the
8
+ HTTP adapter to provide real-time progress updates for large uploads and
9
+ downloads.
10
+ """
11
+
12
+ from typing import Callable, Optional
13
+
14
+
15
+ class ProgressTracker:
16
+ """
17
+ Track upload and download progress for HTTP operations.
18
+
19
+ This class maintains progress state for data transfer operations and
20
+ invokes a callback function whenever progress is updated. It is designed
21
+ to work with both upload and download scenarios, tracking bytes transferred
22
+ against the total expected size (when known).
23
+
24
+ The progress callback receives two arguments:
25
+ - loaded (int): Number of bytes transferred so far
26
+ - total (int): Total bytes expected (0 if unknown)
27
+
28
+ Attributes:
29
+ callback (Optional[Callable]): Function called on each progress update.
30
+ Signature: callback(loaded: int, total: int)
31
+ total (int): Total bytes expected to transfer (0 if unknown)
32
+ loaded (int): Number of bytes transferred so far
33
+ """
34
+
35
+ def __init__(self, callback: Optional[Callable] = None):
36
+ """
37
+ Initialize a new progress tracker.
38
+
39
+ Args:
40
+ callback (Optional[Callable]): Function to call on progress updates.
41
+ Receives (loaded, total) arguments.
42
+ If None, progress is tracked but no
43
+ notifications are sent.
44
+ """
45
+ self.callback = callback
46
+ self.total = 0
47
+ self.loaded = 0
48
+
49
+ def update(self, loaded: int, total: Optional[int] = None) -> None:
50
+ """
51
+ Update the current progress and notify callback if present.
52
+
53
+ This method updates the amount of data transferred and optionally
54
+ sets the total expected size. After updating internal state, it
55
+ invokes the callback function (if provided) with the current
56
+ progress values.
57
+
58
+ Args:
59
+ loaded (int): Current number of bytes transferred
60
+ total (Optional[int]): Total bytes expected to transfer.
61
+ If provided, updates self.total.
62
+ If None, self.total remains unchanged.
63
+
64
+ Example:
65
+ >>> tracker = ProgressTracker(lambda l, t: print(f"{l}/{t}"))
66
+ >>> tracker.update(0, 100) # Start: 0/100 bytes
67
+ >>> tracker.update(50) # Halfway: 50/100 bytes
68
+ >>> tracker.update(100) # Complete: 100/100 bytes
69
+ """
70
+ self.loaded = loaded
71
+ if total is not None:
72
+ self.total = total
73
+
74
+ if self.callback:
75
+ self.callback(self.loaded, self.total)
76
+
77
+ def reset(self) -> None:
78
+ """
79
+ Reset the progress tracker to initial state.
80
+
81
+ This method resets both loaded and total counters to zero.
82
+ Useful when reusing the same tracker for multiple operations.
83
+
84
+ Note:
85
+ This does not call the callback function. The callback will
86
+ be called on the next update() call with the reset values.
87
+ """
88
+ self.loaded = 0
89
+ self.total = 0
@@ -0,0 +1,5 @@
1
+ from .request_transform import RequestTransformer
2
+ from .response_transform import ResponseTransformer
3
+ from .data_serializer import DataSerializer
4
+
5
+ __all__ = ['RequestTransformer', 'ResponseTransformer', 'DataSerializer']
@@ -0,0 +1,96 @@
1
+ """
2
+ Data Serializer Module
3
+ ----------------------
4
+ Provides data serialization and deserialization utilities for different formats.
5
+
6
+ This module contains the DataSerializer class with static methods for converting
7
+ between Python data structures and various wire formats used in HTTP requests
8
+ and responses, including JSON and URL-encoded form data.
9
+ """
10
+
11
+ import json
12
+ from typing import Any
13
+
14
+
15
+ class DataSerializer:
16
+ """
17
+ Serialize and deserialize data for different content types.
18
+
19
+ This utility class provides static methods for common data format
20
+ conversions needed in HTTP communication. It handles JSON serialization
21
+ and parsing, as well as URL-encoded form data serialization.
22
+
23
+ All methods are static, so the class is not meant to be instantiated.
24
+ """
25
+
26
+ @staticmethod
27
+ def serialize_json(data: Any) -> str:
28
+ """
29
+ Serialize Python object to JSON string.
30
+
31
+ Converts Python data structures (dict, list, str, int, float, bool, None)
32
+ to a JSON formatted string. This is the inverse of to_json().
33
+
34
+ Args:
35
+ data (Any): Python object to serialize. Must be JSON-serializable.
36
+ Common types: dict, list, str, int, float, bool, None
37
+
38
+ Returns:
39
+ str: JSON string representation of the input data
40
+
41
+ Raises:
42
+ TypeError: If data contains non-serializable types
43
+ ValueError: If circular references are detected
44
+
45
+ Example:
46
+ >>> DataSerializer.serialize_json({'name': 'John', 'age': 30})
47
+ '{"name": "John", "age": 30}'
48
+ """
49
+ return json.dumps(data)
50
+
51
+ @staticmethod
52
+ def serialize_form_data(data: dict) -> str:
53
+ """
54
+ Serialize dictionary to URL-encoded form data string.
55
+
56
+ Converts a dictionary of key-value pairs into a URL-encoded string
57
+ suitable for use as application/x-www-form-urlencoded body.
58
+ Values are automatically percent-encoded.
59
+
60
+ Args:
61
+ data (dict): Dictionary of form fields. Keys and values are
62
+ converted to strings and URL-encoded.
63
+
64
+ Returns:
65
+ str: URL-encoded string in format 'key1=value1&key2=value2'
66
+
67
+ Example:
68
+ >>> DataSerializer.serialize_form_data({'name': 'John Doe', 'age': 30})
69
+ 'name=John%20Doe&age=30'
70
+ """
71
+ from urllib.parse import urlencode
72
+ return urlencode(data)
73
+
74
+ @staticmethod
75
+ def to_json(data: str) -> Any:
76
+ """
77
+ Parse JSON string to Python object.
78
+
79
+ Deserializes a JSON formatted string back into a Python data structure.
80
+ This is the inverse of serialize_json().
81
+
82
+ Args:
83
+ data (str): JSON string to parse
84
+
85
+ Returns:
86
+ Any: Python object (dict, list, str, int, float, bool, None)
87
+ depending on the JSON content
88
+
89
+ Raises:
90
+ json.JSONDecodeError: If the string is not valid JSON
91
+
92
+ Example:
93
+ >>> DataSerializer.to_json('{"name": "John", "age": 30}')
94
+ {'name': 'John', 'age': 30}
95
+ """
96
+ return json.loads(data)
@@ -0,0 +1,96 @@
1
+ """
2
+ Request Transformer Module
3
+ --------------------------
4
+ Handles transformation of request data before sending.
5
+
6
+ This module provides the RequestTransformer class which applies various
7
+ transformations to request configurations before they are sent over the
8
+ network. Transformations include custom transform functions, automatic
9
+ Content-Type header setting, and Basic authentication header generation.
10
+ """
11
+
12
+ import json
13
+ import base64
14
+ from typing import Any
15
+ from ..core.config import RequestConfig
16
+
17
+
18
+ class RequestTransformer:
19
+ """
20
+ Transform request data before it is sent over the network.
21
+
22
+ This class applies a series of transformations to RequestConfig objects
23
+ to prepare them for HTTP transmission. Transformations are applied in
24
+ a specific order:
25
+ 1. Custom transformRequest function (if provided by user)
26
+ 2. Automatic Content-Type header detection and setting
27
+ 3. Basic authentication header generation (if auth credentials provided)
28
+
29
+ The transformer is idempotent and can be called multiple times without
30
+ adverse effects, though it's designed to be called once per request.
31
+ """
32
+
33
+ def transform(self, config: RequestConfig) -> RequestConfig:
34
+ """
35
+ Apply all request transformations to the configuration.
36
+
37
+ This method modifies the request configuration in place and returns
38
+ it for method chaining. Transformations are applied in sequence:
39
+
40
+ 1. Custom transformation: If config.transformRequest is provided,
41
+ it is called with the current data and the result becomes the
42
+ new data.
43
+
44
+ 2. Content-Type auto-detection: If data is present and no
45
+ Content-Type header is set, the header is automatically added
46
+ based on data type (dict -> JSON, str -> text/plain).
47
+
48
+ 3. Basic Authentication: If config.auth contains username and
49
+ password, an Authorization header with Basic scheme is added.
50
+
51
+ Args:
52
+ config (RequestConfig): The request configuration to transform
53
+
54
+ Returns:
55
+ RequestConfig: The same configuration object after transformations
56
+ (modified in place for efficiency)
57
+
58
+ Example:
59
+ >>> transformer = RequestTransformer()
60
+ >>> config = RequestConfig(
61
+ ... data={'name': 'John'},
62
+ ... auth={'username': 'user', 'password': 'pass'}
63
+ ... )
64
+ >>> transformed = transformer.transform(config)
65
+ >>> print(transformed.headers['Content-Type'])
66
+ 'application/json'
67
+ >>> print(transformed.headers['Authorization'])
68
+ 'Basic dXNlcjpwYXNz'
69
+ """
70
+ # Apply custom request transformation if provided by user
71
+ # This allows users to modify data before any auto-transformations
72
+ if config.transformRequest:
73
+ config.data = config.transformRequest(config.data)
74
+
75
+ # Automatically set Content-Type header based on data type
76
+ # Only applies if data exists and no Content-Type is already set
77
+ if config.data and 'Content-Type' not in config.headers:
78
+ if isinstance(config.data, dict):
79
+ # Dictionary data -> JSON format
80
+ config.headers['Content-Type'] = 'application/json'
81
+ config.data = json.dumps(config.data)
82
+ elif isinstance(config.data, str):
83
+ # String data -> plain text
84
+ config.headers['Content-Type'] = 'text/plain'
85
+
86
+ # Handle Basic Authentication
87
+ # If auth dictionary contains username and password, generate Basic auth header
88
+ if config.auth:
89
+ # Combine username and password with colon separator
90
+ auth_str = f"{config.auth.get('username', '')}:{config.auth.get('password', '')}"
91
+ # Base64 encode the credentials
92
+ encoded = base64.b64encode(auth_str.encode()).decode()
93
+ # Add the Authorization header
94
+ config.headers['Authorization'] = f"Basic {encoded}"
95
+
96
+ return config
@@ -0,0 +1,73 @@
1
+ """
2
+ Response Transformer Module
3
+ ---------------------------
4
+ Handles transformation of response data after receiving.
5
+
6
+ This module provides the ResponseTransformer class which applies transformations
7
+ to response data before it is returned to the caller. Transformations include
8
+ custom transformResponse functions provided by the user.
9
+ """
10
+
11
+ import json
12
+ from ..core.response import Response
13
+
14
+
15
+ class ResponseTransformer:
16
+ """
17
+ Transform response data after it is received from the network.
18
+
19
+ This class applies user-defined transformations to response data before
20
+ the response object is returned to the caller. The primary use case is
21
+ the transformResponse function that users can configure to modify or
22
+ preprocess response data.
23
+
24
+ Transformations are applied in order:
25
+ 1. Custom transformResponse function (if provided by user)
26
+
27
+ The transformer is idempotent and can be called multiple times without
28
+ adverse effects, though it's designed to be called once per response.
29
+ """
30
+
31
+ def transform(self, response: Response) -> Response:
32
+ """
33
+ Apply all response transformations to the response object.
34
+
35
+ This method modifies the response data in place and returns the
36
+ response object for method chaining. The primary transformation is:
37
+
38
+ 1. Custom transformation: If response.config.transformResponse is
39
+ provided, it is called with the current response data and the
40
+ result becomes the new response data.
41
+
42
+ This allows users to implement features like:
43
+ - Automatic date parsing
44
+ - Data normalization
45
+ - Error structure standardization
46
+ - Response caching
47
+ - Logging or analytics
48
+
49
+ Args:
50
+ response (Response): The response object to transform
51
+
52
+ Returns:
53
+ Response: The same response object after transformations
54
+ (modified in place for efficiency)
55
+
56
+ Example:
57
+ >>> transformer = ResponseTransformer()
58
+ >>> config = RequestConfig(transformResponse=lambda data: data.get('result', {}))
59
+ >>> response = Response(
60
+ ... data={'result': {'id': 1, 'name': 'John'}},
61
+ ... config=config,
62
+ ... ...
63
+ ... )
64
+ >>> transformed = transformer.transform(response)
65
+ >>> print(transformed.data)
66
+ {'id': 1, 'name': 'John'}
67
+ """
68
+ # Apply custom response transformation if provided by user
69
+ # This allows users to modify or extract data before it reaches their code
70
+ if response.config.transformResponse:
71
+ response.data = response.config.transformResponse(response.data)
72
+
73
+ return response
@@ -0,0 +1,5 @@
1
+ from .helpers import merge_headers, build_url, parse_params
2
+ from .cookies import CookieManager
3
+ from .redirect import RedirectHandler
4
+
5
+ __all__ = ['merge_headers', 'build_url', 'parse_params', 'CookieManager', 'RedirectHandler']
@@ -0,0 +1,128 @@
1
+ """
2
+ Cookie Manager Module
3
+ ---------------------
4
+ Provides HTTP cookie handling for the AtomHTTP client.
5
+
6
+ This module contains the CookieManager class which manages HTTP cookies
7
+ for client sessions. It supports setting, retrieving, and formatting cookies
8
+ for requests, as well as parsing cookies from server responses.
9
+ """
10
+
11
+ from http.cookies import SimpleCookie
12
+ from typing import Dict, Optional
13
+
14
+
15
+ class CookieManager:
16
+ """
17
+ Manage HTTP cookies for client sessions.
18
+
19
+ This class wraps Python's SimpleCookie to provide a convenient interface
20
+ for cookie management in HTTP client operations. It supports:
21
+ - Setting cookies with additional attributes (domain, path, expires, etc.)
22
+ - Retrieving cookie values by name
23
+ - Generating Cookie header values for requests
24
+ - Parsing Set-Cookie headers from responses
25
+
26
+ The manager maintains cookies across requests when the same client
27
+ instance is used, enabling session persistence.
28
+
29
+ Attributes:
30
+ _cookies (SimpleCookie): Internal SimpleCookie object storing all cookies
31
+ """
32
+
33
+ def __init__(self):
34
+ """
35
+ Initialize an empty cookie manager.
36
+
37
+ No cookies are present initially. Cookies can be added via set_cookie()
38
+ or loaded from response headers via load_from_response().
39
+ """
40
+ self._cookies = SimpleCookie()
41
+
42
+ def set_cookie(self, name: str, value: str, **kwargs) -> None:
43
+ """
44
+ Set a cookie with optional attributes.
45
+
46
+ This method sets a cookie value and optionally adds attributes such
47
+ as domain, path, expires, secure, httponly, etc.
48
+
49
+ Args:
50
+ name (str): Cookie name
51
+ value (str): Cookie value
52
+ **kwargs: Optional cookie attributes (domain, path, expires, max-age,
53
+ secure, httponly, samesite, etc.)
54
+
55
+ Example:
56
+ >>> manager = CookieManager()
57
+ >>> manager.set_cookie('session', 'abc123', domain='.example.com', path='/')
58
+ >>> manager.set_cookie('user', 'john', max_age=3600, secure=True)
59
+ """
60
+ self._cookies[name] = value
61
+ for key, val in kwargs.items():
62
+ self._cookies[name][key] = val
63
+
64
+ def get_cookie(self, name: str) -> Optional[str]:
65
+ """
66
+ Get the value of a cookie by name.
67
+
68
+ Args:
69
+ name (str): Name of the cookie to retrieve
70
+
71
+ Returns:
72
+ Optional[str]: Cookie value if found, None otherwise
73
+
74
+ Example:
75
+ >>> manager = CookieManager()
76
+ >>> manager.set_cookie('session', 'abc123')
77
+ >>> value = manager.get_cookie('session')
78
+ >>> print(value)
79
+ 'abc123'
80
+ """
81
+ if name in self._cookies:
82
+ return self._cookies[name].value
83
+ return None
84
+
85
+ def get_header(self) -> str:
86
+ """
87
+ Generate the Cookie header value for HTTP requests.
88
+
89
+ This method formats all stored cookies into a string suitable for
90
+ the Cookie header. Cookies are formatted as "name=value" pairs
91
+ separated by semicolons.
92
+
93
+ Returns:
94
+ str: Cookie header value (empty string if no cookies)
95
+
96
+ Example:
97
+ >>> manager = CookieManager()
98
+ >>> manager.set_cookie('session', 'abc123')
99
+ >>> manager.set_cookie('user', 'john')
100
+ >>> print(manager.get_header())
101
+ 'session=abc123; user=john'
102
+ """
103
+ return '; '.join([f"{k}={v.value}" for k, v in self._cookies.items()])
104
+
105
+ def load_from_response(self, header: str) -> None:
106
+ """
107
+ Parse and load cookies from a Set-Cookie response header.
108
+
109
+ This method processes Set-Cookie headers from server responses and
110
+ adds the cookies to the manager. Existing cookies with the same
111
+ name may be overwritten according to cookie precedence rules.
112
+
113
+ Args:
114
+ header (str): The Set-Cookie header value from an HTTP response
115
+
116
+ Example:
117
+ >>> manager = CookieManager()
118
+ >>> set_cookie = "session=abc123; Path=/; HttpOnly"
119
+ >>> manager.load_from_response(set_cookie)
120
+ >>> print(manager.get_cookie('session'))
121
+ 'abc123'
122
+
123
+ Note:
124
+ This method uses SimpleCookie.load() which parses the cookie
125
+ string according to RFC 6265. Multiple cookies in a single
126
+ header are supported as per the standard.
127
+ """
128
+ self._cookies.load(header)
@@ -0,0 +1,111 @@
1
+ """
2
+ URL and Header Utilities
3
+ ------------------------
4
+ Helper functions for URL construction, header merging, and parameter parsing.
5
+
6
+ This module provides utility functions for common operations needed in HTTP
7
+ client operations, including merging headers, building URLs with query
8
+ parameters, and parsing parameter dictionaries.
9
+ """
10
+
11
+ from typing import Dict, Any, Optional
12
+ from urllib.parse import urlencode, urljoin
13
+
14
+
15
+ def merge_headers(default: Dict[str, str], custom: Dict[str, str]) -> Dict[str, str]:
16
+ """
17
+ Merge default headers with custom headers.
18
+
19
+ Custom headers take precedence over default headers when keys conflict.
20
+ This function creates a new dictionary rather than modifying the originals.
21
+
22
+ Args:
23
+ default (Dict[str, str]): Default headers (base values)
24
+ custom (Dict[str, str]): Custom headers that override defaults
25
+
26
+ Returns:
27
+ Dict[str, str]: Merged headers dictionary where custom headers
28
+ override default headers on key conflicts
29
+
30
+ Example:
31
+ >>> default = {'Accept': 'application/json', 'User-Agent': 'client/1.0'}
32
+ >>> custom = {'Authorization': 'Bearer token', 'Accept': 'text/plain'}
33
+ >>> merged = merge_headers(default, custom)
34
+ >>> print(merged)
35
+ {'Accept': 'text/plain', 'User-Agent': 'client/1.0', 'Authorization': 'Bearer token'}
36
+ """
37
+ merged = default.copy()
38
+ merged.update(custom)
39
+ return merged
40
+
41
+
42
+ def build_url(
43
+ base_url: str,
44
+ path: str,
45
+ params: Optional[Dict[str, Any]] = None
46
+ ) -> str:
47
+ """
48
+ Build a complete URL by combining base URL, path, and query parameters.
49
+
50
+ This function handles both absolute and relative paths. If the path
51
+ already contains a protocol (http:// or https://), the base_url is
52
+ ignored and the path is used as the full URL.
53
+
54
+ Args:
55
+ base_url (str): Base URL (e.g., 'https://api.example.com')
56
+ path (str): URL path (e.g., '/users' or 'https://other.com/api')
57
+ params (Optional[Dict[str, Any]]): Query parameters to append
58
+
59
+ Returns:
60
+ str: Complete URL with path and query parameters properly encoded
61
+
62
+ Example:
63
+ >>> build_url('https://api.example.com', '/users', {'page': 1, 'limit': 10})
64
+ 'https://api.example.com/users?page=1&limit=10'
65
+
66
+ >>> build_url('https://api.example.com', 'https://other.com/api')
67
+ 'https://other.com/api'
68
+
69
+ >>> build_url('https://api.example.com', '/search', {'q': 'python http'})
70
+ 'https://api.example.com/search?q=python%20http'
71
+ """
72
+ # If path is absolute (has protocol), use it as the full URL
73
+ if path.startswith(('http://', 'https://')):
74
+ url = path
75
+ else:
76
+ # Combine base URL and relative path
77
+ url = urljoin(base_url, path)
78
+
79
+ # Append query parameters if provided
80
+ if params:
81
+ query_string = urlencode(params)
82
+ # Add parameters as query string (appending if already exists)
83
+ if '?' not in url:
84
+ url = f"{url}?{query_string}"
85
+ else:
86
+ url = f"{url}&{query_string}"
87
+
88
+ return url
89
+
90
+
91
+ def parse_params(params: Dict[str, Any]) -> Dict[str, str]:
92
+ """
93
+ Parse and convert parameter dictionary to string-keyed dictionary.
94
+
95
+ This function filters out None values and converts all values to strings,
96
+ preparing them for URL encoding or other string-based operations.
97
+
98
+ Args:
99
+ params (Dict[str, Any]): Input parameters with any value types
100
+
101
+ Returns:
102
+ Dict[str, str]: Filtered dictionary with None values removed and
103
+ all values converted to strings
104
+
105
+ Example:
106
+ >>> params = {'page': 1, 'limit': 10, 'sort': None, 'active': True}
107
+ >>> parsed = parse_params(params)
108
+ >>> print(parsed)
109
+ {'page': '1', 'limit': '10', 'active': 'True'}
110
+ """
111
+ return {k: str(v) for k, v in params.items() if v is not None}