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
|
@@ -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,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,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}
|