cloudru-qa-http-client 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.
- cloudru_qa_http_client-1.0.0.dist-info/METADATA +615 -0
- cloudru_qa_http_client-1.0.0.dist-info/RECORD +18 -0
- cloudru_qa_http_client-1.0.0.dist-info/WHEEL +4 -0
- cloudru_qa_http_client-1.0.0.dist-info/licenses/LICENSE +202 -0
- http_client/.agents/skills/http-client-usage/SKILL.md +227 -0
- http_client/.agents/skills/http-client-usage/references/options.md +219 -0
- http_client/.agents/skills/http-client-usage/references/pitfalls.md +53 -0
- http_client/__init__.py +16 -0
- http_client/client.py +603 -0
- http_client/config.py +82 -0
- http_client/event_hooks.py +159 -0
- http_client/options.py +226 -0
- http_client/py.typed +1 -0
- http_client/types.py +141 -0
- http_client/utils/__init__.py +0 -0
- http_client/utils/logutils.py +275 -0
- http_client/utils/sanitizer.py +59 -0
- http_client/utils/traceparent.py +12 -0
http_client/config.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from httpx._config import DEFAULT_MAX_REDIRECTS
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
import ssl
|
|
13
|
+
|
|
14
|
+
from httpx._types import AuthTypes, TimeoutTypes, URLTypes
|
|
15
|
+
|
|
16
|
+
from .types import RequestHook, ResponseHook, SendDecorator, StreamRequestDecorator
|
|
17
|
+
|
|
18
|
+
DEFAULT_OCTET_STREAM_LOG_MAXLENGTH = 1024
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(kw_only=True, slots=True)
|
|
22
|
+
class HttpClientConfig:
|
|
23
|
+
"""Transport settings collected by options before creating the HTTP client."""
|
|
24
|
+
|
|
25
|
+
base_url: URLTypes
|
|
26
|
+
verify: ssl.SSLContext | str | bool = True
|
|
27
|
+
auth: AuthTypes | None = None
|
|
28
|
+
timeout: TimeoutTypes = field(default_factory=lambda: httpx.Timeout(60))
|
|
29
|
+
url_suffix: str = ""
|
|
30
|
+
request_hooks: list[RequestHook] = field(default_factory=list)
|
|
31
|
+
response_hooks: list[ResponseHook] = field(default_factory=list)
|
|
32
|
+
send_decorators: list[SendDecorator] = field(default_factory=list)
|
|
33
|
+
stream_decorators: list[StreamRequestDecorator] = field(default_factory=list)
|
|
34
|
+
follow_redirects: bool = False
|
|
35
|
+
max_redirects: int = DEFAULT_MAX_REDIRECTS
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(kw_only=True)
|
|
39
|
+
class LoggingConfig:
|
|
40
|
+
"""HTTP log formatting and length limit settings."""
|
|
41
|
+
|
|
42
|
+
log_level: int = logging.INFO
|
|
43
|
+
json_indent: int | None = None
|
|
44
|
+
use_line_breaks: bool = False
|
|
45
|
+
octet_stream_log_maxlength: int | None = DEFAULT_OCTET_STREAM_LOG_MAXLENGTH
|
|
46
|
+
plain_text_log_maxlength: int | None = None
|
|
47
|
+
json_log_maxlength: int | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
LOGGING_CONFIG = LoggingConfig()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def configure_logging(options: LoggingConfig | None = None) -> None:
|
|
54
|
+
"""Configure HTTP log formatting using environment variables.
|
|
55
|
+
|
|
56
|
+
:param options: Default logging settings
|
|
57
|
+
"""
|
|
58
|
+
settings = options or LoggingConfig()
|
|
59
|
+
if log_level_env := os.getenv("HTTP_CLIENT_LOG_LEVEL", None):
|
|
60
|
+
LOGGING_CONFIG.log_level = int(log_level_env)
|
|
61
|
+
else:
|
|
62
|
+
LOGGING_CONFIG.log_level = settings.log_level
|
|
63
|
+
if use_line_breaks_env := os.getenv("HTTP_CLIENT_USE_LINE_BREAKS", None):
|
|
64
|
+
LOGGING_CONFIG.use_line_breaks = use_line_breaks_env.lower() in ["true", "y", "yes", "on"]
|
|
65
|
+
else:
|
|
66
|
+
LOGGING_CONFIG.use_line_breaks = settings.use_line_breaks
|
|
67
|
+
if json_indent_env := os.getenv("HTTP_CLIENT_JSON_INDENT", None):
|
|
68
|
+
LOGGING_CONFIG.json_indent = int(json_indent_env)
|
|
69
|
+
else:
|
|
70
|
+
LOGGING_CONFIG.json_indent = settings.json_indent
|
|
71
|
+
if octet_stream_log_maxlength_env := os.getenv("HTTP_CLIENT_OCTET_STREAM_LOG_MAXLENGTH", None):
|
|
72
|
+
LOGGING_CONFIG.octet_stream_log_maxlength = int(octet_stream_log_maxlength_env)
|
|
73
|
+
else:
|
|
74
|
+
LOGGING_CONFIG.octet_stream_log_maxlength = settings.octet_stream_log_maxlength
|
|
75
|
+
if plain_text_log_maxlength_env := os.getenv("HTTP_CLIENT_PLAIN_TEXT_LOG_MAXLENGTH", None):
|
|
76
|
+
LOGGING_CONFIG.plain_text_log_maxlength = int(plain_text_log_maxlength_env)
|
|
77
|
+
else:
|
|
78
|
+
LOGGING_CONFIG.plain_text_log_maxlength = settings.plain_text_log_maxlength
|
|
79
|
+
if json_log_maxlength_env := os.getenv("HTTP_CLIENT_JSON_LOG_MAXLENGTH", None):
|
|
80
|
+
LOGGING_CONFIG.json_log_maxlength = int(json_log_maxlength_env)
|
|
81
|
+
else:
|
|
82
|
+
LOGGING_CONFIG.json_log_maxlength = settings.json_log_maxlength
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from httpx import Request, Response
|
|
6
|
+
|
|
7
|
+
from .config import LOGGING_CONFIG as CFG
|
|
8
|
+
from .types import RequestHook, ResponseHook
|
|
9
|
+
from .utils import logutils, traceparent
|
|
10
|
+
|
|
11
|
+
REQUEST_ID_HEADER = "X-Request-ID"
|
|
12
|
+
TRACEPARENT_HEADER = "Traceparent"
|
|
13
|
+
|
|
14
|
+
# Always mask these: leaking a single secret in logs is far worse
|
|
15
|
+
# than an unnecessary asterisk.
|
|
16
|
+
DEFAULT_SANITIZE_KEYS: tuple[str, ...] = (
|
|
17
|
+
"authorization",
|
|
18
|
+
"proxy-authorization",
|
|
19
|
+
"cookie",
|
|
20
|
+
"set-cookie",
|
|
21
|
+
"x-api-key",
|
|
22
|
+
"api-key",
|
|
23
|
+
"api_key",
|
|
24
|
+
"access_token",
|
|
25
|
+
"refresh_token",
|
|
26
|
+
"password",
|
|
27
|
+
"secret",
|
|
28
|
+
"token",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def request_logger_hook(
|
|
33
|
+
logger: logging.Logger,
|
|
34
|
+
sanitize_keys: Sequence[str],
|
|
35
|
+
*,
|
|
36
|
+
json_indent: int | None = None,
|
|
37
|
+
use_line_breaks: bool | None = None,
|
|
38
|
+
log_level: int | None = None,
|
|
39
|
+
) -> RequestHook:
|
|
40
|
+
"""Create a hook for safe HTTP request logging.
|
|
41
|
+
|
|
42
|
+
:param logger: Log record recipient
|
|
43
|
+
:param sanitize_keys: Fields to mask
|
|
44
|
+
:param json_indent: JSON indentation
|
|
45
|
+
:param use_line_breaks: Multiline formatting
|
|
46
|
+
:param log_level: Log level
|
|
47
|
+
:returns: Request hook
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def log_request_hook(request: Request) -> None:
|
|
51
|
+
log_msg = logutils.create_request_log(
|
|
52
|
+
r=request,
|
|
53
|
+
sanitize_keys=sanitize_keys,
|
|
54
|
+
json_indent=_indent(json_indent),
|
|
55
|
+
use_line_breaks=_line_breaks(use_line_breaks=use_line_breaks),
|
|
56
|
+
)
|
|
57
|
+
logger.log(level=_level(log_level), msg=log_msg)
|
|
58
|
+
|
|
59
|
+
return log_request_hook
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def response_logger_hook(
|
|
63
|
+
logger: logging.Logger,
|
|
64
|
+
sanitize_keys: Sequence[str],
|
|
65
|
+
*,
|
|
66
|
+
json_indent: int | None = None,
|
|
67
|
+
use_line_breaks: bool | None = None,
|
|
68
|
+
log_level: int | None = None,
|
|
69
|
+
) -> ResponseHook:
|
|
70
|
+
"""Create a hook for safe HTTP response logging.
|
|
71
|
+
|
|
72
|
+
:param logger: Log record recipient
|
|
73
|
+
:param sanitize_keys: Fields to mask
|
|
74
|
+
:param json_indent: JSON indentation
|
|
75
|
+
:param use_line_breaks: Multiline formatting
|
|
76
|
+
:param log_level: Log level
|
|
77
|
+
:returns: Response hook
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def log_response_hook(response: Response) -> None:
|
|
81
|
+
log_msg = logutils.create_response_log(
|
|
82
|
+
r=response,
|
|
83
|
+
sanitize_keys=sanitize_keys,
|
|
84
|
+
json_indent=_indent(json_indent),
|
|
85
|
+
use_line_breaks=_line_breaks(use_line_breaks=use_line_breaks),
|
|
86
|
+
)
|
|
87
|
+
logger.log(level=_level(log_level), msg=log_msg)
|
|
88
|
+
|
|
89
|
+
return log_response_hook
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def async_response_logger_hook(
|
|
93
|
+
logger: logging.Logger,
|
|
94
|
+
sanitize_keys: Sequence[str],
|
|
95
|
+
*,
|
|
96
|
+
json_indent: int | None = None,
|
|
97
|
+
use_line_breaks: bool | None = None,
|
|
98
|
+
log_level: int | None = None,
|
|
99
|
+
) -> ResponseHook:
|
|
100
|
+
"""Build a response logging hook for an asynchronous client.
|
|
101
|
+
|
|
102
|
+
A separate implementation is needed: when the hook runs, the asynchronous
|
|
103
|
+
response body has not been read and its stream is only accessible through await.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
async def log_response_hook(response: Response) -> None:
|
|
107
|
+
log_msg = await logutils.create_response_log_async(
|
|
108
|
+
r=response,
|
|
109
|
+
sanitize_keys=sanitize_keys,
|
|
110
|
+
json_indent=_indent(json_indent),
|
|
111
|
+
use_line_breaks=_line_breaks(use_line_breaks=use_line_breaks),
|
|
112
|
+
)
|
|
113
|
+
logger.log(level=_level(log_level), msg=log_msg)
|
|
114
|
+
|
|
115
|
+
return log_response_hook
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def request_id_hook(request: Request) -> None:
|
|
119
|
+
"""Set a request ID if it is missing.
|
|
120
|
+
|
|
121
|
+
Preserve the existing value deliberately. The hook runs every time a request object
|
|
122
|
+
is sent, and retries send the same object: overwriting the ID would give
|
|
123
|
+
each attempt a new ID, preventing correlation in logs. For the same reason,
|
|
124
|
+
preserve the ID supplied by the caller.
|
|
125
|
+
"""
|
|
126
|
+
request.headers.setdefault(REQUEST_ID_HEADER, str(uuid4()))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def traceparent_hook(request: Request) -> None:
|
|
130
|
+
"""Set traceparent if it is missing. See :func:`request_id_hook` for details."""
|
|
131
|
+
request.headers.setdefault(TRACEPARENT_HEADER, traceparent.generate())
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def add_default_headers_hook(headers: dict[str, str]) -> RequestHook:
|
|
135
|
+
"""Create a hook to add missing request headers.
|
|
136
|
+
|
|
137
|
+
:param headers: Default headers
|
|
138
|
+
:returns: Request hook
|
|
139
|
+
"""
|
|
140
|
+
defaults = dict(headers)
|
|
141
|
+
|
|
142
|
+
def add_default_headers(request: Request) -> None:
|
|
143
|
+
for name, value in defaults.items():
|
|
144
|
+
if name not in request.headers:
|
|
145
|
+
request.headers[name] = value
|
|
146
|
+
|
|
147
|
+
return add_default_headers
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _level(log_level: int | None) -> int:
|
|
151
|
+
return log_level if log_level is not None else CFG.log_level
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _indent(json_indent: int | None) -> int | None:
|
|
155
|
+
return json_indent if json_indent is not None else CFG.json_indent
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _line_breaks(*, use_line_breaks: bool | None) -> bool:
|
|
159
|
+
return use_line_breaks if use_line_breaks is not None else CFG.use_line_breaks
|
http_client/options.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import ssl
|
|
3
|
+
from logging import Logger
|
|
4
|
+
from typing import TYPE_CHECKING, Unpack, cast
|
|
5
|
+
|
|
6
|
+
from httpx._config import DEFAULT_MAX_REDIRECTS
|
|
7
|
+
from httpx._types import AuthTypes, TimeoutTypes
|
|
8
|
+
|
|
9
|
+
from .client import AsyncHttpClient, BaseHttpClient, ClientOption, _reject_unknown_options
|
|
10
|
+
from .event_hooks import (
|
|
11
|
+
DEFAULT_SANITIZE_KEYS,
|
|
12
|
+
add_default_headers_hook,
|
|
13
|
+
async_response_logger_hook,
|
|
14
|
+
request_id_hook,
|
|
15
|
+
request_logger_hook,
|
|
16
|
+
response_logger_hook,
|
|
17
|
+
traceparent_hook,
|
|
18
|
+
)
|
|
19
|
+
from .types import LoggingOptions, RequestHook, ResponseHook, SendDecorator, StreamRequestDecorator
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from collections.abc import Callable
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def with_timeout(timeout: TimeoutTypes) -> ClientOption:
|
|
26
|
+
"""Set the HTTP client timeout.
|
|
27
|
+
|
|
28
|
+
:param timeout: Timeout
|
|
29
|
+
:returns: Option that sets the HTTP client timeout
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def option(client: BaseHttpClient) -> None:
|
|
33
|
+
client.timeout = timeout
|
|
34
|
+
|
|
35
|
+
return option
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def with_verify_context(*, verify: ssl.SSLContext | str | bool) -> ClientOption:
|
|
39
|
+
"""Set the SSL certificate verification context for HTTPS connections.
|
|
40
|
+
|
|
41
|
+
`verify=False` disables certificate verification completely and makes the connection
|
|
42
|
+
vulnerable to man-in-the-middle attacks. Use it only for local debugging.
|
|
43
|
+
|
|
44
|
+
:param verify: SSL context, CA certificate path, or False to disable verification
|
|
45
|
+
:returns: Option that sets the HTTP client certificate verification context
|
|
46
|
+
"""
|
|
47
|
+
# httpx deprecated string verify values in 0.28, so convert paths to contexts here.
|
|
48
|
+
context = ssl.create_default_context(cafile=verify) if isinstance(verify, str) else verify
|
|
49
|
+
|
|
50
|
+
def option(client: BaseHttpClient) -> None:
|
|
51
|
+
client.verify = context
|
|
52
|
+
|
|
53
|
+
return option
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def use_truststore() -> ClientOption:
|
|
57
|
+
"""Use the system SSL certificate store to verify HTTPS connections.
|
|
58
|
+
|
|
59
|
+
:raises ImportError: If the truststore module is not installed
|
|
60
|
+
:return: Option that sets the certificate verification context using the system store.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
truststore = importlib.import_module("truststore")
|
|
64
|
+
|
|
65
|
+
context_factory = cast("Callable[[int], ssl.SSLContext]", truststore.SSLContext)
|
|
66
|
+
ctx = context_factory(ssl.PROTOCOL_TLS_CLIENT)
|
|
67
|
+
return with_verify_context(verify=ctx)
|
|
68
|
+
except ImportError:
|
|
69
|
+
message = "Для использования truststore установите cloudru-qa-http-client[truststore]"
|
|
70
|
+
raise ImportError(message) from None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def with_auth(auth: AuthTypes) -> ClientOption:
|
|
74
|
+
"""Set HTTP client authentication.
|
|
75
|
+
|
|
76
|
+
:param auth: Authentication credentials
|
|
77
|
+
:returns: Option that sets HTTP client authentication
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def option(client: BaseHttpClient) -> None:
|
|
81
|
+
client.auth = auth
|
|
82
|
+
|
|
83
|
+
return option
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def with_autogenerated_request_id() -> ClientOption:
|
|
87
|
+
"""Set an automatically generated ID for each HTTP client request.
|
|
88
|
+
|
|
89
|
+
:returns: Option that enables automatic request IDs for the HTTP client
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def option(client: BaseHttpClient) -> None:
|
|
93
|
+
client.add_request_hook(request_id_hook, prepend=True)
|
|
94
|
+
|
|
95
|
+
return option
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def with_autogenerated_traceparent() -> ClientOption:
|
|
99
|
+
"""Set an automatically generated trace ID for each HTTP client request.
|
|
100
|
+
|
|
101
|
+
:returns: Option that enables automatic trace IDs for the HTTP client
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def option(client: BaseHttpClient) -> None:
|
|
105
|
+
client.add_request_hook(traceparent_hook, prepend=True)
|
|
106
|
+
|
|
107
|
+
return option
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def with_send_decorator(decorator: SendDecorator) -> ClientOption:
|
|
111
|
+
"""Add a decorator to the HTTP client send method.
|
|
112
|
+
|
|
113
|
+
:param decorator: Decorator to apply to the send method
|
|
114
|
+
:returns: Option that adds a decorator to the HTTP client
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def option(client: BaseHttpClient) -> None:
|
|
118
|
+
client.add_send_decorator(decorator)
|
|
119
|
+
|
|
120
|
+
return option
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def with_stream_decorator(decorator: StreamRequestDecorator) -> ClientOption:
|
|
124
|
+
"""Add a decorator to the HTTP client stream_request method.
|
|
125
|
+
|
|
126
|
+
:param decorator: Decorator to apply to the stream_request method
|
|
127
|
+
:returns: Option that adds a decorator to the HTTP client
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
def option(client: BaseHttpClient) -> None:
|
|
131
|
+
client.add_stream_decorator(decorator)
|
|
132
|
+
|
|
133
|
+
return option
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def with_logger(logger: Logger, **settings: Unpack[LoggingOptions]) -> ClientOption:
|
|
137
|
+
"""Set the logger for HTTP client requests and responses.
|
|
138
|
+
|
|
139
|
+
:param logger: Logger
|
|
140
|
+
:param sanitize_keys: Keys in [glom](https://glom.readthedocs.io/en/latest/) format
|
|
141
|
+
to mask sensitive data
|
|
142
|
+
:param json_indent: JSON log indentation width (no indentation by default)
|
|
143
|
+
:param use_line_breaks: Whether to add line breaks between request properties (disabled by default)
|
|
144
|
+
:param log_level: HTTP client log level, INFO by default
|
|
145
|
+
:param use_default_sanitize_keys: Whether to mask common secrets
|
|
146
|
+
(see `DEFAULT_SANITIZE_KEYS`) in addition to `sanitize_keys`
|
|
147
|
+
:returns: Option that sets the HTTP client logger and hooks
|
|
148
|
+
"""
|
|
149
|
+
_reject_unknown_options(settings, LoggingOptions.__optional_keys__)
|
|
150
|
+
sanitize_keys = settings.get("sanitize_keys")
|
|
151
|
+
json_indent = settings.get("json_indent")
|
|
152
|
+
use_line_breaks = settings.get("use_line_breaks")
|
|
153
|
+
log_level = settings.get("log_level")
|
|
154
|
+
use_default_sanitize_keys = settings.get("use_default_sanitize_keys", True)
|
|
155
|
+
keys = list(sanitize_keys or [])
|
|
156
|
+
if use_default_sanitize_keys:
|
|
157
|
+
keys.extend(key for key in DEFAULT_SANITIZE_KEYS if key not in keys)
|
|
158
|
+
|
|
159
|
+
def option(client: BaseHttpClient) -> None:
|
|
160
|
+
client.add_request_hook(
|
|
161
|
+
request_logger_hook(
|
|
162
|
+
logger=logger,
|
|
163
|
+
sanitize_keys=keys,
|
|
164
|
+
json_indent=json_indent,
|
|
165
|
+
use_line_breaks=use_line_breaks,
|
|
166
|
+
log_level=log_level,
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
build_response_hook = (
|
|
170
|
+
async_response_logger_hook if isinstance(client, AsyncHttpClient) else response_logger_hook
|
|
171
|
+
)
|
|
172
|
+
client.add_response_hook(
|
|
173
|
+
build_response_hook(
|
|
174
|
+
logger=logger,
|
|
175
|
+
sanitize_keys=keys,
|
|
176
|
+
json_indent=json_indent,
|
|
177
|
+
use_line_breaks=use_line_breaks,
|
|
178
|
+
log_level=log_level,
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return option
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def with_default_headers(headers: dict[str, str]) -> ClientOption:
|
|
186
|
+
"""Set default HTTP client headers.
|
|
187
|
+
|
|
188
|
+
:param headers: Header dictionary
|
|
189
|
+
:returns: Option that sets default HTTP client headers
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
def option(client: BaseHttpClient) -> None:
|
|
193
|
+
client.add_request_hook(add_default_headers_hook(headers))
|
|
194
|
+
|
|
195
|
+
return option
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def with_request_hook(hook: RequestHook) -> ClientOption:
|
|
199
|
+
"""Set a hook to process HTTP requests before sending them."""
|
|
200
|
+
|
|
201
|
+
def option(client: BaseHttpClient) -> None:
|
|
202
|
+
client.add_request_hook(hook)
|
|
203
|
+
|
|
204
|
+
return option
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def with_response_hook(hook: ResponseHook) -> ClientOption:
|
|
208
|
+
"""Set a hook to process HTTP responses after receiving them."""
|
|
209
|
+
|
|
210
|
+
def option(client: BaseHttpClient) -> None:
|
|
211
|
+
client.add_response_hook(hook)
|
|
212
|
+
|
|
213
|
+
return option
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def with_follow_redirects(max_redirects: int = DEFAULT_MAX_REDIRECTS) -> ClientOption:
|
|
217
|
+
"""Enable HTTP client redirects.
|
|
218
|
+
|
|
219
|
+
:param max_redirects: Maximum number of redirects
|
|
220
|
+
:returns: Option that enables HTTP client redirects
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
def option(client: BaseHttpClient) -> None:
|
|
224
|
+
client.enable_redirects(max_redirects)
|
|
225
|
+
|
|
226
|
+
return option
|
http_client/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This marker file indicates that the package supports type checking under PEP 561
|
http_client/types.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
2
|
+
from contextlib import AbstractAsyncContextManager, AbstractContextManager
|
|
3
|
+
from typing import Protocol, TypedDict
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from httpx._client import UseClientDefault
|
|
7
|
+
from httpx._types import (
|
|
8
|
+
AuthTypes,
|
|
9
|
+
CookieTypes,
|
|
10
|
+
HeaderTypes,
|
|
11
|
+
QueryParamTypes,
|
|
12
|
+
RequestContent,
|
|
13
|
+
RequestFiles,
|
|
14
|
+
TimeoutTypes,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
type EventHook[T = object] = Callable[[T], Awaitable[None] | None]
|
|
18
|
+
type RequestHook = EventHook[httpx.Request]
|
|
19
|
+
type ResponseHook = EventHook[httpx.Response]
|
|
20
|
+
type RequestData = Mapping[str, object]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LoggingOptions(TypedDict, total=False):
|
|
24
|
+
"""Named logging settings for an individual HTTP client."""
|
|
25
|
+
|
|
26
|
+
sanitize_keys: Sequence[str] | None
|
|
27
|
+
json_indent: int | None
|
|
28
|
+
use_line_breaks: bool | None
|
|
29
|
+
log_level: int | None
|
|
30
|
+
use_default_sanitize_keys: bool
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class QueryOptions(TypedDict, total=False):
|
|
34
|
+
"""Named options for an HTTP request without a body."""
|
|
35
|
+
|
|
36
|
+
params: QueryParamTypes | None
|
|
37
|
+
headers: HeaderTypes | None
|
|
38
|
+
cookies: CookieTypes | None
|
|
39
|
+
auth: AuthTypes | None
|
|
40
|
+
follow_redirects: bool | None
|
|
41
|
+
timeout: TimeoutTypes | UseClientDefault
|
|
42
|
+
extensions: dict[str, object] | None
|
|
43
|
+
anonymous: bool
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RequestOptions(QueryOptions, total=False):
|
|
47
|
+
"""Named options for an HTTP request with a body."""
|
|
48
|
+
|
|
49
|
+
content: RequestContent | None
|
|
50
|
+
data: RequestData | None
|
|
51
|
+
files: RequestFiles | None
|
|
52
|
+
json: object
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class StreamOptions(TypedDict, total=False):
|
|
56
|
+
"""Options for sending a prepared request in streaming mode."""
|
|
57
|
+
|
|
58
|
+
auth: AuthTypes | None
|
|
59
|
+
follow_redirects: bool | None
|
|
60
|
+
anonymous: bool
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class SendOptions(StreamOptions, total=False):
|
|
64
|
+
"""Options for sending a prepared HTTP request."""
|
|
65
|
+
|
|
66
|
+
stream: bool
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SyncSendMethod(Protocol):
|
|
70
|
+
"""Protocol for a synchronous send method."""
|
|
71
|
+
|
|
72
|
+
def __call__(
|
|
73
|
+
self,
|
|
74
|
+
request: httpx.Request,
|
|
75
|
+
*,
|
|
76
|
+
stream: bool = False,
|
|
77
|
+
auth: AuthTypes | None = None,
|
|
78
|
+
follow_redirects: bool | None = None,
|
|
79
|
+
anonymous: bool = False,
|
|
80
|
+
) -> httpx.Response:
|
|
81
|
+
"""Send a prepared HTTP request and return the response."""
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class AsyncSendMethod(Protocol):
|
|
86
|
+
"""Protocol for an asynchronous send method."""
|
|
87
|
+
|
|
88
|
+
async def __call__(
|
|
89
|
+
self,
|
|
90
|
+
request: httpx.Request,
|
|
91
|
+
*,
|
|
92
|
+
stream: bool = False,
|
|
93
|
+
auth: AuthTypes | None = None,
|
|
94
|
+
follow_redirects: bool | None = None,
|
|
95
|
+
anonymous: bool = False,
|
|
96
|
+
) -> httpx.Response:
|
|
97
|
+
"""Send a prepared HTTP request asynchronously and return the response."""
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class SyncStreamRequestMethod(Protocol):
|
|
102
|
+
"""Protocol for a synchronous stream_request method."""
|
|
103
|
+
|
|
104
|
+
def __call__(
|
|
105
|
+
self,
|
|
106
|
+
request: httpx.Request,
|
|
107
|
+
*,
|
|
108
|
+
anonymous: bool = False,
|
|
109
|
+
auth: AuthTypes | None = None,
|
|
110
|
+
follow_redirects: bool | None = None,
|
|
111
|
+
) -> AbstractContextManager[httpx.Response]:
|
|
112
|
+
"""Open a streaming HTTP response context."""
|
|
113
|
+
...
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class AsyncStreamRequestMethod(Protocol):
|
|
117
|
+
"""Protocol for an asynchronous stream_request method."""
|
|
118
|
+
|
|
119
|
+
def __call__(
|
|
120
|
+
self,
|
|
121
|
+
request: httpx.Request,
|
|
122
|
+
*,
|
|
123
|
+
anonymous: bool = False,
|
|
124
|
+
auth: AuthTypes | None = None,
|
|
125
|
+
follow_redirects: bool | None = None,
|
|
126
|
+
) -> AbstractAsyncContextManager[httpx.Response]:
|
|
127
|
+
"""Open an asynchronous streaming HTTP response context."""
|
|
128
|
+
...
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# Types for send method decorators
|
|
132
|
+
SyncSendDecorator = Callable[[SyncSendMethod], SyncSendMethod]
|
|
133
|
+
AsyncSendDecorator = Callable[[AsyncSendMethod], AsyncSendMethod]
|
|
134
|
+
|
|
135
|
+
SendDecorator = SyncSendDecorator | AsyncSendDecorator
|
|
136
|
+
|
|
137
|
+
# Types for stream_request method decorators
|
|
138
|
+
SyncStreamRequestDecorator = Callable[[SyncStreamRequestMethod], SyncStreamRequestMethod]
|
|
139
|
+
AsyncStreamRequestDecorator = Callable[[AsyncStreamRequestMethod], AsyncStreamRequestMethod]
|
|
140
|
+
|
|
141
|
+
StreamRequestDecorator = SyncStreamRequestDecorator | AsyncStreamRequestDecorator
|
|
File without changes
|