httpcorexyz 1.0.10__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,154 @@
1
+ from ._api import request, stream
2
+ from ._async import (
3
+ AsyncConnectionInterface,
4
+ AsyncConnectionPool,
5
+ AsyncHTTP2Connection,
6
+ AsyncHTTP11Connection,
7
+ AsyncHTTPConnection,
8
+ AsyncHTTPProxy,
9
+ AsyncSOCKSProxy,
10
+ )
11
+ from ._backends.base import (
12
+ SOCKET_OPTION,
13
+ AsyncNetworkBackend,
14
+ AsyncNetworkStream,
15
+ NetworkBackend,
16
+ NetworkStream,
17
+ )
18
+ from ._backends.mock import AsyncMockBackend, AsyncMockStream, MockBackend, MockStream
19
+ from ._backends.sync import SyncBackend
20
+ from ._exceptions import (
21
+ ConnectError,
22
+ ConnectionNotAvailable,
23
+ ConnectTimeout,
24
+ LocalProtocolError,
25
+ NetworkError,
26
+ PoolTimeout,
27
+ ProtocolError,
28
+ ProxyError,
29
+ ReadError,
30
+ ReadTimeout,
31
+ RemoteProtocolError,
32
+ TimeoutException,
33
+ UnsupportedProtocol,
34
+ WriteError,
35
+ WriteTimeout,
36
+ )
37
+ from ._models import URL, Origin, Proxy, Request, Response
38
+ from ._ssl import default_ssl_context
39
+ from ._sync import (
40
+ ConnectionInterface,
41
+ ConnectionPool,
42
+ HTTP2Connection,
43
+ HTTP11Connection,
44
+ HTTPConnection,
45
+ HTTPProxy,
46
+ SOCKSProxy,
47
+ )
48
+
49
+ # The 'httpcorexyz.AnyIOBackend' class is conditional on 'anyio' being installed.
50
+ try:
51
+ from ._backends.anyio import AnyIOBackend
52
+ except ImportError: # pragma: nocover
53
+
54
+ class AnyIOBackend: # type: ignore
55
+ def __init__(self, *args, **kwargs): # type: ignore
56
+ msg = "Attempted to use 'httpcorexyz.AnyIOBackend' but 'anyio' is not installed."
57
+ raise RuntimeError(msg)
58
+
59
+
60
+ # The 'httpcorexyz.TrioBackend' class is conditional on 'trio' being installed.
61
+ try:
62
+ from ._backends.trio import TrioBackend
63
+ except ImportError: # pragma: nocover
64
+
65
+ class TrioBackend: # type: ignore
66
+ def __init__(self, *args, **kwargs): # type: ignore
67
+ msg = "Attempted to use 'httpcorexyz.TrioBackend' but 'trio' is not installed."
68
+ raise RuntimeError(msg)
69
+
70
+
71
+ __all__ = [
72
+ # top-level requests
73
+ "request",
74
+ "stream",
75
+ # models
76
+ "Origin",
77
+ "URL",
78
+ "Request",
79
+ "Response",
80
+ "Proxy",
81
+ # async
82
+ "AsyncHTTPConnection",
83
+ "AsyncConnectionPool",
84
+ "AsyncHTTPProxy",
85
+ "AsyncHTTP11Connection",
86
+ "AsyncHTTP2Connection",
87
+ "AsyncConnectionInterface",
88
+ "AsyncSOCKSProxy",
89
+ # sync
90
+ "HTTPConnection",
91
+ "ConnectionPool",
92
+ "HTTPProxy",
93
+ "HTTP11Connection",
94
+ "HTTP2Connection",
95
+ "ConnectionInterface",
96
+ "SOCKSProxy",
97
+ # network backends, implementations
98
+ "SyncBackend",
99
+ "AnyIOBackend",
100
+ "TrioBackend",
101
+ # network backends, mock implementations
102
+ "AsyncMockBackend",
103
+ "AsyncMockStream",
104
+ "MockBackend",
105
+ "MockStream",
106
+ # network backends, interface
107
+ "AsyncNetworkStream",
108
+ "AsyncNetworkBackend",
109
+ "NetworkStream",
110
+ "NetworkBackend",
111
+ # util
112
+ "default_ssl_context",
113
+ "SOCKET_OPTION",
114
+ # exceptions
115
+ "ConnectionNotAvailable",
116
+ "ProxyError",
117
+ "ProtocolError",
118
+ "LocalProtocolError",
119
+ "RemoteProtocolError",
120
+ "UnsupportedProtocol",
121
+ "TimeoutException",
122
+ "PoolTimeout",
123
+ "ConnectTimeout",
124
+ "ReadTimeout",
125
+ "WriteTimeout",
126
+ "NetworkError",
127
+ "ConnectError",
128
+ "ReadError",
129
+ "WriteError",
130
+ ]
131
+
132
+ __version__ = "1.0.10"
133
+
134
+
135
+ __locals = locals()
136
+ for __name in __all__:
137
+ # Exclude SOCKET_OPTION, it causes AttributeError on Python 3.14
138
+ if not __name.startswith(("__", "SOCKET_OPTION")):
139
+ setattr(__locals[__name], "__module__", "httpcorexyz") # noqa
140
+
141
+ import logging as _logging
142
+ import sys as _sys
143
+
144
+ if "httpcore" not in _sys.modules:
145
+ _sys.modules["httpcore"] = _sys.modules[__name__]
146
+ _logging.getLogger("httpcorexyz").debug(
147
+ "httpcore not found in sys.modules — aliased to httpcorexyz"
148
+ )
149
+ else: # pragma: no cover
150
+ _logging.getLogger("httpcorexyz").debug(
151
+ "httpcore already present in sys.modules — httpcorexyz alias not applied"
152
+ )
153
+
154
+ del _logging, _sys
httpcorexyz/_api.py ADDED
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import typing
5
+
6
+ from ._models import URL, Extensions, HeaderTypes, Response
7
+ from ._sync.connection_pool import ConnectionPool
8
+
9
+
10
+ def request(
11
+ method: bytes | str,
12
+ url: URL | bytes | str,
13
+ *,
14
+ headers: HeaderTypes = None,
15
+ content: bytes | typing.Iterator[bytes] | None = None,
16
+ extensions: Extensions | None = None,
17
+ ) -> Response:
18
+ """
19
+ Sends an HTTP request, returning the response.
20
+
21
+ ```
22
+ response = httpcore.request("GET", "https://www.example.com/")
23
+ ```
24
+
25
+ Arguments:
26
+ method: The HTTP method for the request. Typically one of `"GET"`,
27
+ `"OPTIONS"`, `"HEAD"`, `"POST"`, `"PUT"`, `"PATCH"`, or `"DELETE"`.
28
+ url: The URL of the HTTP request. Either as an instance of `httpcore.URL`,
29
+ or as str/bytes.
30
+ headers: The HTTP request headers. Either as a dictionary of str/bytes,
31
+ or as a list of two-tuples of str/bytes.
32
+ content: The content of the request body. Either as bytes,
33
+ or as a bytes iterator.
34
+ extensions: A dictionary of optional extra information included on the request.
35
+ Possible keys include `"timeout"`.
36
+
37
+ Returns:
38
+ An instance of `httpcore.Response`.
39
+ """
40
+ with ConnectionPool() as pool:
41
+ return pool.request(
42
+ method=method,
43
+ url=url,
44
+ headers=headers,
45
+ content=content,
46
+ extensions=extensions,
47
+ )
48
+
49
+
50
+ @contextlib.contextmanager
51
+ def stream(
52
+ method: bytes | str,
53
+ url: URL | bytes | str,
54
+ *,
55
+ headers: HeaderTypes = None,
56
+ content: bytes | typing.Iterator[bytes] | None = None,
57
+ extensions: Extensions | None = None,
58
+ ) -> typing.Iterator[Response]:
59
+ """
60
+ Sends an HTTP request, returning the response within a content manager.
61
+
62
+ ```
63
+ with httpcore.stream("GET", "https://www.example.com/") as response:
64
+ ...
65
+ ```
66
+
67
+ When using the `stream()` function, the body of the response will not be
68
+ automatically read. If you want to access the response body you should
69
+ either use `content = response.read()`, or `for chunk in response.iter_content()`.
70
+
71
+ Arguments:
72
+ method: The HTTP method for the request. Typically one of `"GET"`,
73
+ `"OPTIONS"`, `"HEAD"`, `"POST"`, `"PUT"`, `"PATCH"`, or `"DELETE"`.
74
+ url: The URL of the HTTP request. Either as an instance of `httpcore.URL`,
75
+ or as str/bytes.
76
+ headers: The HTTP request headers. Either as a dictionary of str/bytes,
77
+ or as a list of two-tuples of str/bytes.
78
+ content: The content of the request body. Either as bytes,
79
+ or as a bytes iterator.
80
+ extensions: A dictionary of optional extra information included on the request.
81
+ Possible keys include `"timeout"`.
82
+
83
+ Returns:
84
+ An instance of `httpcore.Response`.
85
+ """
86
+ with ConnectionPool() as pool:
87
+ with pool.stream(
88
+ method=method,
89
+ url=url,
90
+ headers=headers,
91
+ content=content,
92
+ extensions=extensions,
93
+ ) as response:
94
+ yield response
@@ -0,0 +1,39 @@
1
+ from .connection import AsyncHTTPConnection
2
+ from .connection_pool import AsyncConnectionPool
3
+ from .http11 import AsyncHTTP11Connection
4
+ from .http_proxy import AsyncHTTPProxy
5
+ from .interfaces import AsyncConnectionInterface
6
+
7
+ try:
8
+ from .http2 import AsyncHTTP2Connection
9
+ except ImportError: # pragma: nocover
10
+
11
+ class AsyncHTTP2Connection: # type: ignore
12
+ def __init__(self, *args, **kwargs) -> None: # type: ignore
13
+ raise RuntimeError(
14
+ "Attempted to use http2 support, but the `h2` package is not "
15
+ "installed. Use 'pip install httpcore[http2]'."
16
+ )
17
+
18
+
19
+ try:
20
+ from .socks_proxy import AsyncSOCKSProxy
21
+ except ImportError: # pragma: nocover
22
+
23
+ class AsyncSOCKSProxy: # type: ignore
24
+ def __init__(self, *args, **kwargs) -> None: # type: ignore
25
+ raise RuntimeError(
26
+ "Attempted to use SOCKS support, but the `socksio` package is not "
27
+ "installed. Use 'pip install httpcore[socks]'."
28
+ )
29
+
30
+
31
+ __all__ = [
32
+ "AsyncHTTPConnection",
33
+ "AsyncConnectionPool",
34
+ "AsyncHTTPProxy",
35
+ "AsyncHTTP11Connection",
36
+ "AsyncHTTP2Connection",
37
+ "AsyncConnectionInterface",
38
+ "AsyncSOCKSProxy",
39
+ ]
@@ -0,0 +1,222 @@
1
+ from __future__ import annotations
2
+
3
+ import itertools
4
+ import logging
5
+ import ssl
6
+ import types
7
+ import typing
8
+
9
+ from .._backends.auto import AutoBackend
10
+ from .._backends.base import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream
11
+ from .._exceptions import ConnectError, ConnectTimeout
12
+ from .._models import Origin, Request, Response
13
+ from .._ssl import default_ssl_context
14
+ from .._synchronization import AsyncLock
15
+ from .._trace import Trace
16
+ from .http11 import AsyncHTTP11Connection
17
+ from .interfaces import AsyncConnectionInterface
18
+
19
+ RETRIES_BACKOFF_FACTOR = 0.5 # 0s, 0.5s, 1s, 2s, 4s, etc.
20
+
21
+
22
+ logger = logging.getLogger("httpcorexyz.connection")
23
+
24
+
25
+ def exponential_backoff(factor: float) -> typing.Iterator[float]:
26
+ """
27
+ Generate a geometric sequence that has a ratio of 2 and starts with 0.
28
+
29
+ For example:
30
+ - `factor = 2`: `0, 2, 4, 8, 16, 32, 64, ...`
31
+ - `factor = 3`: `0, 3, 6, 12, 24, 48, 96, ...`
32
+ """
33
+ yield 0
34
+ for n in itertools.count():
35
+ yield factor * 2**n
36
+
37
+
38
+ class AsyncHTTPConnection(AsyncConnectionInterface):
39
+ def __init__(
40
+ self,
41
+ origin: Origin,
42
+ ssl_context: ssl.SSLContext | None = None,
43
+ keepalive_expiry: float | None = None,
44
+ http1: bool = True,
45
+ http2: bool = False,
46
+ retries: int = 0,
47
+ local_address: str | None = None,
48
+ uds: str | None = None,
49
+ network_backend: AsyncNetworkBackend | None = None,
50
+ socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
51
+ ) -> None:
52
+ self._origin = origin
53
+ self._ssl_context = ssl_context
54
+ self._keepalive_expiry = keepalive_expiry
55
+ self._http1 = http1
56
+ self._http2 = http2
57
+ self._retries = retries
58
+ self._local_address = local_address
59
+ self._uds = uds
60
+
61
+ self._network_backend: AsyncNetworkBackend = (
62
+ AutoBackend() if network_backend is None else network_backend
63
+ )
64
+ self._connection: AsyncConnectionInterface | None = None
65
+ self._connect_failed: bool = False
66
+ self._request_lock = AsyncLock()
67
+ self._socket_options = socket_options
68
+
69
+ async def handle_async_request(self, request: Request) -> Response:
70
+ if not self.can_handle_request(request.url.origin):
71
+ raise RuntimeError(
72
+ f"Attempted to send request to {request.url.origin} on connection to {self._origin}"
73
+ )
74
+
75
+ try:
76
+ async with self._request_lock:
77
+ if self._connection is None:
78
+ stream = await self._connect(request)
79
+
80
+ ssl_object = stream.get_extra_info("ssl_object")
81
+ http2_negotiated = (
82
+ ssl_object is not None
83
+ and ssl_object.selected_alpn_protocol() == "h2"
84
+ )
85
+ if http2_negotiated or (self._http2 and not self._http1):
86
+ from .http2 import AsyncHTTP2Connection
87
+
88
+ self._connection = AsyncHTTP2Connection(
89
+ origin=self._origin,
90
+ stream=stream,
91
+ keepalive_expiry=self._keepalive_expiry,
92
+ )
93
+ else:
94
+ self._connection = AsyncHTTP11Connection(
95
+ origin=self._origin,
96
+ stream=stream,
97
+ keepalive_expiry=self._keepalive_expiry,
98
+ )
99
+ except BaseException as exc:
100
+ self._connect_failed = True
101
+ raise exc
102
+
103
+ return await self._connection.handle_async_request(request)
104
+
105
+ async def _connect(self, request: Request) -> AsyncNetworkStream:
106
+ timeouts = request.extensions.get("timeout", {})
107
+ sni_hostname = request.extensions.get("sni_hostname", None)
108
+ timeout = timeouts.get("connect", None)
109
+
110
+ retries_left = self._retries
111
+ delays = exponential_backoff(factor=RETRIES_BACKOFF_FACTOR)
112
+
113
+ while True:
114
+ try:
115
+ if self._uds is None:
116
+ kwargs = {
117
+ "host": self._origin.host.decode("ascii"),
118
+ "port": self._origin.port,
119
+ "local_address": self._local_address,
120
+ "timeout": timeout,
121
+ "socket_options": self._socket_options,
122
+ }
123
+ async with Trace("connect_tcp", logger, request, kwargs) as trace:
124
+ stream = await self._network_backend.connect_tcp(**kwargs)
125
+ trace.return_value = stream
126
+ else:
127
+ kwargs = {
128
+ "path": self._uds,
129
+ "timeout": timeout,
130
+ "socket_options": self._socket_options,
131
+ }
132
+ async with Trace(
133
+ "connect_unix_socket", logger, request, kwargs
134
+ ) as trace:
135
+ stream = await self._network_backend.connect_unix_socket(
136
+ **kwargs
137
+ )
138
+ trace.return_value = stream
139
+
140
+ if self._origin.scheme in (b"https", b"wss"):
141
+ ssl_context = (
142
+ default_ssl_context()
143
+ if self._ssl_context is None
144
+ else self._ssl_context
145
+ )
146
+ alpn_protocols = ["http/1.1", "h2"] if self._http2 else ["http/1.1"]
147
+ ssl_context.set_alpn_protocols(alpn_protocols)
148
+
149
+ kwargs = {
150
+ "ssl_context": ssl_context,
151
+ "server_hostname": sni_hostname
152
+ or self._origin.host.decode("ascii"),
153
+ "timeout": timeout,
154
+ }
155
+ async with Trace("start_tls", logger, request, kwargs) as trace:
156
+ stream = await stream.start_tls(**kwargs)
157
+ trace.return_value = stream
158
+ return stream
159
+ except (ConnectError, ConnectTimeout):
160
+ if retries_left <= 0:
161
+ raise
162
+ retries_left -= 1
163
+ delay = next(delays)
164
+ async with Trace("retry", logger, request, kwargs) as trace:
165
+ await self._network_backend.sleep(delay)
166
+
167
+ def can_handle_request(self, origin: Origin) -> bool:
168
+ return origin == self._origin
169
+
170
+ async def aclose(self) -> None:
171
+ if self._connection is not None:
172
+ async with Trace("close", logger, None, {}):
173
+ await self._connection.aclose()
174
+
175
+ def is_available(self) -> bool:
176
+ if self._connection is None:
177
+ # If HTTP/2 support is enabled, and the resulting connection could
178
+ # end up as HTTP/2 then we should indicate the connection as being
179
+ # available to service multiple requests.
180
+ return (
181
+ self._http2
182
+ and (self._origin.scheme == b"https" or not self._http1)
183
+ and not self._connect_failed
184
+ )
185
+ return self._connection.is_available()
186
+
187
+ def has_expired(self) -> bool:
188
+ if self._connection is None:
189
+ return self._connect_failed
190
+ return self._connection.has_expired()
191
+
192
+ def is_idle(self) -> bool:
193
+ if self._connection is None:
194
+ return self._connect_failed
195
+ return self._connection.is_idle()
196
+
197
+ def is_closed(self) -> bool:
198
+ if self._connection is None:
199
+ return self._connect_failed
200
+ return self._connection.is_closed()
201
+
202
+ def info(self) -> str:
203
+ if self._connection is None:
204
+ return "CONNECTION FAILED" if self._connect_failed else "CONNECTING"
205
+ return self._connection.info()
206
+
207
+ def __repr__(self) -> str:
208
+ return f"<{self.__class__.__name__} [{self.info()}]>"
209
+
210
+ # These context managers are not used in the standard flow, but are
211
+ # useful for testing or working with connection instances directly.
212
+
213
+ async def __aenter__(self) -> AsyncHTTPConnection:
214
+ return self
215
+
216
+ async def __aexit__(
217
+ self,
218
+ exc_type: type[BaseException] | None = None,
219
+ exc_value: BaseException | None = None,
220
+ traceback: types.TracebackType | None = None,
221
+ ) -> None:
222
+ await self.aclose()