pathapi 0.1.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.
Files changed (104) hide show
  1. pathapi/AGENTS.md +2355 -0
  2. pathapi/README.md +205 -0
  3. pathapi/__init__.py +62 -0
  4. pathapi/_commit.py +116 -0
  5. pathapi/_discard.py +92 -0
  6. pathapi/_errorpolicy.py +106 -0
  7. pathapi/_filestat.py +194 -0
  8. pathapi/_lru.py +138 -0
  9. pathapi/_names.py +70 -0
  10. pathapi/_permissions.py +102 -0
  11. pathapi/_stubs.py +76 -0
  12. pathapi/_tree.py +136 -0
  13. pathapi/archive/__init__.py +44 -0
  14. pathapi/archive/_backend.py +393 -0
  15. pathapi/archive/_names.py +63 -0
  16. pathapi/archive/_pack.py +394 -0
  17. pathapi/archive/_path.py +352 -0
  18. pathapi/archive/_snapshot.py +269 -0
  19. pathapi/archive/_tar.py +126 -0
  20. pathapi/archive/_write.py +359 -0
  21. pathapi/archive/_zip.py +563 -0
  22. pathapi/archive/_zipentries.py +272 -0
  23. pathapi/checksum.py +61 -0
  24. pathapi/exceptions.py +22 -0
  25. pathapi/fspath.py +513 -0
  26. pathapi/ftp/__init__.py +27 -0
  27. pathapi/ftp/_backend.py +418 -0
  28. pathapi/ftp/_facts.py +117 -0
  29. pathapi/ftp/_path.py +582 -0
  30. pathapi/glob/__init__.py +27 -0
  31. pathapi/glob/_entries.py +103 -0
  32. pathapi/glob/_patterns.py +164 -0
  33. pathapi/glob/_recursion.py +180 -0
  34. pathapi/glob/_selection.py +303 -0
  35. pathapi/http/__init__.py +84 -0
  36. pathapi/http/_body.py +156 -0
  37. pathapi/http/_builtin.py +553 -0
  38. pathapi/http/_client.py +211 -0
  39. pathapi/http/_errors.py +79 -0
  40. pathapi/http/_follow.py +230 -0
  41. pathapi/http/_headers.py +233 -0
  42. pathapi/http/_pool.py +169 -0
  43. pathapi/http/_proxies.py +142 -0
  44. pathapi/http/_response.py +255 -0
  45. pathapi/http/_settings.py +12 -0
  46. pathapi/http/_status.py +68 -0
  47. pathapi/http/_transports.py +173 -0
  48. pathapi/http/testing/__init__.py +80 -0
  49. pathapi/http/testing/__main__.py +10 -0
  50. pathapi/http/testing/suite/conftest.py +98 -0
  51. pathapi/http/testing/suite/test_client_default.py +285 -0
  52. pathapi/http/testing/suite/test_client_errors.py +181 -0
  53. pathapi/http/testing/suite/test_client_peer_text.py +86 -0
  54. pathapi/http/testing/suite/test_client_redirect_origin.py +134 -0
  55. pathapi/http/testing/suite/test_client_redirects.py +231 -0
  56. pathapi/http/testing/suite/test_client_reply_limits.py +201 -0
  57. pathapi/http/testing/suite/test_client_write_redirects.py +239 -0
  58. pathapi/http/testing/suite/test_transport_body.py +302 -0
  59. pathapi/http/testing/suite/test_transport_send.py +662 -0
  60. pathapi/http/testing/suite/transport_fakes.py +133 -0
  61. pathapi/http/testing/suite/transport_guard.py +174 -0
  62. pathapi/http/testing/suite/transport_wire.py +511 -0
  63. pathapi/mempath.py +438 -0
  64. pathapi/path/__init__.py +19 -0
  65. pathapi/path/_info.py +10 -0
  66. pathapi/path/_path.py +198 -0
  67. pathapi/path/_pure.py +522 -0
  68. pathapi/path/_relation.py +10 -0
  69. pathapi/path/_transfer.py +10 -0
  70. pathapi/protocols/__init__.py +35 -0
  71. pathapi/protocols/_bundles.py +22 -0
  72. pathapi/protocols/_errors.py +12 -0
  73. pathapi/protocols/_info.py +65 -0
  74. pathapi/protocols/_relation.py +191 -0
  75. pathapi/protocols/_removal.py +69 -0
  76. pathapi/protocols/_support.py +122 -0
  77. pathapi/protocols/_transfer.py +100 -0
  78. pathapi/protocols/checksum.py +80 -0
  79. pathapi/protocols/fs.py +429 -0
  80. pathapi/protocols/io.py +224 -0
  81. pathapi/protocols/links.py +135 -0
  82. pathapi/protocols/pure.py +31 -0
  83. pathapi/protocols/resolve.py +97 -0
  84. pathapi/protocols/transfer.py +656 -0
  85. pathapi/protocols/tree.py +492 -0
  86. pathapi/providers.py +48 -0
  87. pathapi/py.typed +1 -0
  88. pathapi/sync/__init__.py +8 -0
  89. pathapi/sync/_compare.py +157 -0
  90. pathapi/sync/_events.py +97 -0
  91. pathapi/sync/_links.py +93 -0
  92. pathapi/sync/_offer.py +33 -0
  93. pathapi/sync/_paths.py +50 -0
  94. pathapi/sync/_replace.py +98 -0
  95. pathapi/sync/_syncer.py +989 -0
  96. pathapi/testing/__init__.py +96 -0
  97. pathapi/testing/_contract.py +522 -0
  98. pathapi/testing/_fixtures.py +83 -0
  99. pathapi/testing/_read.py +279 -0
  100. pathapi-0.1.0.dist-info/METADATA +242 -0
  101. pathapi-0.1.0.dist-info/RECORD +104 -0
  102. pathapi-0.1.0.dist-info/WHEEL +4 -0
  103. pathapi-0.1.0.dist-info/entry_points.txt +2 -0
  104. pathapi-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,553 @@
1
+ """The built-in transport: HTTP/1.1 on `http.client`, with no third-party code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import errno as _errno
6
+ import http.client as _client
7
+ import logging as _logging
8
+ import select as _select
9
+ import socket as _socket
10
+ import ssl as _ssl
11
+ import threading as _threading
12
+ import time as _time
13
+ import typing as _ty
14
+
15
+ from . import _pool
16
+ from ._body import prepare_body
17
+ from ._headers import (
18
+ Target,
19
+ bracket,
20
+ check_body,
21
+ check_headers,
22
+ check_method,
23
+ check_timeout,
24
+ check_url,
25
+ wants_decoding,
26
+ )
27
+ from ._proxies import ProxyAddress, proxies_for
28
+ from ._response import HTTPResponse
29
+
30
+ _log = _logging.getLogger(__name__)
31
+
32
+ REUSE_WINDOW = 1.0
33
+ """A request that cannot be sent again uses an idle connection only if the
34
+ connection carried a response within this many seconds; an older one may have
35
+ been dropped by the server, and the request would be lost with it."""
36
+
37
+ _IDEMPOTENT = frozenset({"GET", "HEAD", "OPTIONS", "PUT", "DELETE", "PROPFIND"})
38
+
39
+ _now = _time.monotonic
40
+
41
+ _DEFAULT_CONTEXT: "_ssl.SSLContext | None" = None
42
+ _UNVERIFIED_CONTEXT: "_ssl.SSLContext | None" = None
43
+ _DEFAULT_CONTEXT_LOCK = _threading.Lock()
44
+
45
+
46
+ def _default_context() -> "_ssl.SSLContext":
47
+ """One verifying context for every transport that is not given its own."""
48
+ global _DEFAULT_CONTEXT
49
+ with _DEFAULT_CONTEXT_LOCK:
50
+ if _DEFAULT_CONTEXT is None:
51
+ _DEFAULT_CONTEXT = _ssl.create_default_context()
52
+ return _DEFAULT_CONTEXT
53
+
54
+
55
+ def _unverified_context() -> "_ssl.SSLContext":
56
+ """One context that accepts any certificate, for `verify=False`."""
57
+ global _UNVERIFIED_CONTEXT
58
+ with _DEFAULT_CONTEXT_LOCK:
59
+ if _UNVERIFIED_CONTEXT is None:
60
+ context = _ssl.create_default_context()
61
+ context.check_hostname = False
62
+ context.verify_mode = _ssl.CERT_NONE
63
+ _UNVERIFIED_CONTEXT = context
64
+ return _UNVERIFIED_CONTEXT
65
+
66
+
67
+ def _failure(error: BaseException) -> BaseException:
68
+ """`error` of the socket, TLS or `http.client` layer as the exception a
69
+ transport raises: `TimeoutError`, `ConnectionError`, `ssl.SSLError`, or
70
+ else `OSError(EIO)`. The text never holds a URL or a header."""
71
+ if isinstance(error, (_socket.timeout, TimeoutError)):
72
+ return TimeoutError(_errno.ETIMEDOUT, "the request timed out")
73
+ if isinstance(error, _ssl.SSLError):
74
+ if "timed out" in str(error):
75
+ return TimeoutError(_errno.ETIMEDOUT, "the request timed out")
76
+ return error
77
+ if isinstance(error, _client.RemoteDisconnected):
78
+ return ConnectionResetError(
79
+ _errno.ECONNRESET, "the server closed the connection without a response"
80
+ )
81
+ if isinstance(error, ConnectionError):
82
+ return error
83
+ if isinstance(error, _socket.gaierror):
84
+ return ConnectionError(
85
+ _errno.EHOSTUNREACH, "the host name could not be resolved"
86
+ )
87
+ if isinstance(error, _client.NotConnected):
88
+ return ConnectionAbortedError(_errno.ECONNABORTED, "the connection was closed")
89
+ if isinstance(error, _client.IncompleteRead):
90
+ return OSError(_errno.EIO, "the response ended before its stated length")
91
+ if isinstance(error, _client.HTTPException):
92
+ return OSError(
93
+ _errno.EIO, f"the response is not valid HTTP ({type(error).__name__})"
94
+ )
95
+ return error
96
+
97
+
98
+ def _dropped(error: BaseException) -> bool:
99
+ """Whether `error` says the server closed an idle connection (as opposed to
100
+ a request that went wrong): nothing of a response had arrived."""
101
+ return isinstance(
102
+ error,
103
+ (
104
+ ConnectionResetError,
105
+ ConnectionAbortedError,
106
+ BrokenPipeError,
107
+ _ssl.SSLEOFError,
108
+ _ssl.SSLZeroReturnError,
109
+ ),
110
+ )
111
+
112
+
113
+ def _unusable(sock) -> bool:
114
+ """Whether an idle connection must not carry a request: the server closed
115
+ it, or sent bytes nobody asked for, which the next reply would start with."""
116
+ if sock is None:
117
+ return True
118
+ try:
119
+ if getattr(sock, "pending", None) is not None and sock.pending():
120
+ return True
121
+ return bool(_select.select([sock], [], [], 0)[0])
122
+ except (OSError, ValueError):
123
+ return True
124
+
125
+
126
+ MAX_INTERIM_REPLIES = 8
127
+ """Most interim (1xx) replies the transport skips before the reply it returns;
128
+ a server that sends more is not answering."""
129
+
130
+
131
+ def _skip_headers(fp) -> None:
132
+ """Reads the header lines of an interim reply, up to its blank line."""
133
+ for _ in range(100):
134
+ line = fp.readline(65537)
135
+ if len(line) > 65536:
136
+ raise _client.LineTooLong("header line")
137
+ if line in (b"\r\n", b"\n", b""):
138
+ return
139
+ raise _client.HTTPException("got more than 100 headers")
140
+
141
+
142
+ class _Reply(_client.HTTPResponse):
143
+ """A response that goes past every interim (1xx) reply, not only `100`
144
+ (`http.client` takes any other as the final one, and the real reply is then
145
+ read as the answer to the next request), and at most `MAX_INTERIM_REPLIES`
146
+ of them."""
147
+
148
+ _interim = 0
149
+
150
+ def _read_status(self):
151
+ while True:
152
+ version, status, reason = super()._read_status()
153
+ if not 100 <= status < 200:
154
+ return version, status, reason
155
+ self._interim += 1
156
+ if self._interim > MAX_INTERIM_REPLIES:
157
+ raise _client.HTTPException("too many interim replies")
158
+ if status == _client.CONTINUE:
159
+ return version, status, reason # `begin()` skips its headers
160
+ _skip_headers(self.fp)
161
+
162
+
163
+ class _CannotConnect(Exception):
164
+ """No connection could be made to the server or the proxy: the next proxy
165
+ of the route is tried."""
166
+
167
+ def __init__(self, error: BaseException):
168
+ super().__init__()
169
+ self.error = error
170
+
171
+
172
+ class _Dropped(Exception):
173
+ """An idle connection the server had closed."""
174
+
175
+ def __init__(self, error: BaseException):
176
+ super().__init__()
177
+ self.error = error
178
+
179
+
180
+ class _Payload:
181
+ """The request body, whether it can be sent twice, and the
182
+ `Content-Length` to add to the request (`None`: none to add)."""
183
+
184
+ def __init__(self, body, pairs):
185
+ self.repeatable = check_body(body)
186
+ self.value, self.length = prepare_body(body, pairs)
187
+
188
+
189
+ class _Body:
190
+ """The body of one response, read from its connection. The connection goes
191
+ back to the pool once the body has been read to its end; one that is
192
+ closed earlier, fails, or ends short is closed."""
193
+
194
+ def __init__(self, owner, pool, connection, response, expected: "int | None"):
195
+ # The transport stays alive while its reply is open: collecting it
196
+ # would close the connection the reply is reading.
197
+ self._owner = owner
198
+ self._pool = pool
199
+ self._connection = connection
200
+ self._response = response
201
+ self._expected = expected
202
+ self._received = 0
203
+ self._done = False
204
+ if expected == 0:
205
+ self._finish()
206
+
207
+ def _finish(self) -> None:
208
+ if self._done:
209
+ return
210
+ self._done = True
211
+ # Closing the response closes its file object, not the socket; a
212
+ # response left open would make `http.client` refuse the next request.
213
+ self._response.close()
214
+ if self._response.will_close:
215
+ self._pool.discard(self._connection)
216
+ else:
217
+ self._pool.give_back(self._connection, _now())
218
+
219
+ def _abort(self) -> None:
220
+ if not self._done:
221
+ self._done = True
222
+ self._response.close()
223
+ self._pool.discard(self._connection)
224
+
225
+ def read(self, amount: int) -> bytes:
226
+ if self._done:
227
+ return b""
228
+ try:
229
+ chunk = self._response.read1(amount)
230
+ except (OSError, _client.HTTPException) as error:
231
+ self._abort()
232
+ raise _failure(error) from None
233
+ except BaseException:
234
+ self._abort()
235
+ raise
236
+ if chunk:
237
+ self._received += len(chunk)
238
+ if self._response.isclosed():
239
+ self._finish()
240
+ return chunk
241
+ if self._expected is not None and self._received < self._expected:
242
+ self._abort()
243
+ raise OSError(
244
+ _errno.EIO,
245
+ f"the response ended after {self._received} of {self._expected} bytes",
246
+ )
247
+ self._finish()
248
+ return b""
249
+
250
+ def close(self) -> None:
251
+ self._abort()
252
+
253
+
254
+ class HTTPTransport:
255
+ """Sends HTTP requests on the standard library: `http.client` over a socket,
256
+ with TLS from `ssl`, and no third-party code.
257
+
258
+ `send()` makes exactly one request: it follows no redirect, adds no
259
+ credentials and retries only as described below. A transport is a context
260
+ manager, and `close()` closes every connection it holds.
261
+
262
+ `https:` servers are verified against `ssl_context`; without one, against
263
+ `ssl.create_default_context()` (the system trust store). `verify=False`
264
+ is the one way to turn verification off, and `ssl_context` wins over it.
265
+
266
+ `proxies` decides which proxy a request uses. `None` reads the
267
+ environment (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY`, through
268
+ `urllib.request.getproxies()` and `proxy_bypass()`). Otherwise it is an
269
+ object indexed with the request URL: it returns `None` (no proxy), one
270
+ proxy, or an iterable of them to try in order when a connection cannot be
271
+ made; `KeyError` means no proxy and the environment is not asked. A proxy
272
+ is a URL or any object with a `url` attribute; a
273
+ plain `dict` may use the keys `requests` uses (`"http"`, `"all"`, ...).
274
+ Only `http://` proxies are supported (`CONNECT` for `https:`), with Basic
275
+ credentials from the proxy URL; any other scheme is
276
+ `OSError(EPROTONOSUPPORT)`. `{}` sends everything direct.
277
+
278
+ Each thread keeps one idle connection per server (at most 8), which a later
279
+ request reuses unless the server has closed it or sent bytes on it since;
280
+ the connections of a thread are closed when it ends. A
281
+ request that fails before any response byte on a reused connection because
282
+ the server had closed it is sent once more on a new connection, but only
283
+ if it is repeatable (`GET`, `HEAD`, `OPTIONS`, `PUT`, `DELETE`,
284
+ `PROPFIND`) and its body is `None` or in memory. Any other request reuses a
285
+ connection only if it carried a response within the last second."""
286
+
287
+ def __init__(
288
+ self,
289
+ *,
290
+ ssl_context: "_ssl.SSLContext | None" = None,
291
+ verify: bool = True,
292
+ proxies: "_ty.Any" = None,
293
+ ):
294
+ self._ssl_context = ssl_context
295
+ self._verify = bool(verify)
296
+ if not self._verify and ssl_context is None:
297
+ _log.warning(
298
+ "HTTPTransport(verify=False): TLS certificates are not verified"
299
+ )
300
+ self._proxies = proxies
301
+ self._pool = _pool.Pool()
302
+ self._finalizer = _pool.finalize_with(self, self._pool)
303
+
304
+ def __repr__(self) -> str:
305
+ return f"HTTPTransport(verify={self._verify})"
306
+
307
+ def __enter__(self) -> "HTTPTransport":
308
+ return self
309
+
310
+ def __exit__(self, *exc_info) -> None:
311
+ self.close()
312
+
313
+ def close(self) -> None:
314
+ """Closes every connection, idle or in use. Harmless when called again;
315
+ a later request connects afresh."""
316
+ self._pool.close()
317
+
318
+ def _context(self) -> "_ssl.SSLContext":
319
+ if self._ssl_context is not None:
320
+ return self._ssl_context
321
+ return _default_context() if self._verify else _unverified_context()
322
+
323
+ def send(
324
+ self,
325
+ method: str,
326
+ url: str,
327
+ *,
328
+ headers: "_ty.Mapping[str, str]",
329
+ body: "bytes | _ty.BinaryIO | _ty.Iterable[bytes] | None" = None,
330
+ timeout: "float | tuple | None",
331
+ stream: bool = False,
332
+ ) -> HTTPResponse:
333
+ """One request, one response.
334
+
335
+ `headers` are sent as given; `Accept-Encoding: gzip, deflate` is added
336
+ (and a coded body decoded) unless they name an `Accept-Encoding`.
337
+ A `body` whose length is known without reading it goes under a
338
+ `Content-Length`, any other chunked; a `Content-Length` in `headers`
339
+ is used as it is. `timeout` is seconds for the connection and for each wait on the
340
+ server: a number, a `(connect, read)` pair, or `None` to wait for ever.
341
+ With `stream=False` the body has been read when this returns; with
342
+ `stream=True` it is read from the response, which must be closed.
343
+
344
+ Raises `ValueError` before anything is sent for a header or URL with
345
+ a control character, a URL with credentials or another scheme;
346
+ `TimeoutError`, `ConnectionError` and `ssl.SSLError` for the network;
347
+ `OSError(EIO)` for a reply that is not valid HTTP or ends early."""
348
+ method = check_method(method)
349
+ target = check_url(url)
350
+ pairs = check_headers(headers)
351
+ connect_timeout, read_timeout = check_timeout(timeout)
352
+ payload = _Payload(body, pairs)
353
+ names = {name.lower() for name, _ in pairs}
354
+ decode = wants_decoding(pairs)
355
+ # As the server will see it: no fragment, "/" for an empty path.
356
+ plain_url = f"{target.scheme}://{target.authority}{target.path}"
357
+ route = proxies_for(self._proxies, plain_url, target.scheme, target.host)
358
+ failure: "BaseException | None" = None
359
+ for proxy in route:
360
+ try:
361
+ response = self._attempt(
362
+ proxy,
363
+ method,
364
+ target,
365
+ pairs,
366
+ names,
367
+ payload,
368
+ connect_timeout,
369
+ read_timeout,
370
+ plain_url,
371
+ decode,
372
+ )
373
+ except _CannotConnect as refused:
374
+ failure = refused.error
375
+ continue
376
+ return response if stream else response._buffered()
377
+ raise failure
378
+
379
+ def _attempt(
380
+ self,
381
+ proxy: "ProxyAddress | None",
382
+ method: str,
383
+ target: Target,
384
+ pairs,
385
+ names,
386
+ payload: _Payload,
387
+ connect_timeout,
388
+ read_timeout,
389
+ url: str,
390
+ decode: bool,
391
+ ) -> HTTPResponse:
392
+ key = (target.scheme, target.host, target.port, proxy.key if proxy else None)
393
+ repeatable = method in _IDEMPOTENT and payload.repeatable
394
+ scope = _pool.scope_id()
395
+ connection = self._pool.take(key, scope)
396
+ if (
397
+ connection is not None
398
+ and not repeatable
399
+ and _now() - connection.released_at > REUSE_WINDOW
400
+ ):
401
+ self._pool.discard(connection)
402
+ connection = None
403
+ if connection is not None and _unusable(connection.http.sock):
404
+ self._pool.discard(connection)
405
+ connection = None
406
+ reused = connection is not None
407
+ while True:
408
+ if connection is None:
409
+ connection = self._open(
410
+ proxy, target, key, scope, connect_timeout, read_timeout
411
+ )
412
+ reused = False
413
+ sock = connection.http.sock
414
+ try:
415
+ if sock is None:
416
+ raise ConnectionAbortedError(
417
+ _errno.ECONNABORTED, "the connection was closed"
418
+ )
419
+ sock.settimeout(read_timeout)
420
+ except OSError:
421
+ # An idle socket that is already closed: nothing was sent.
422
+ self._pool.discard(connection)
423
+ if not reused:
424
+ raise
425
+ connection, reused = None, False
426
+ continue
427
+ try:
428
+ return self._exchange(
429
+ connection,
430
+ proxy,
431
+ method,
432
+ target,
433
+ pairs,
434
+ names,
435
+ payload,
436
+ url,
437
+ decode,
438
+ reused,
439
+ )
440
+ except _Dropped as dropped:
441
+ if reused and repeatable:
442
+ connection, reused = None, False
443
+ continue
444
+ raise dropped.error from None
445
+
446
+ def _headers(
447
+ self, proxy: "ProxyAddress | None", target: Target, pairs, names, payload
448
+ ) -> "dict[str, str]":
449
+ sent = dict(pairs)
450
+ if payload.length is not None:
451
+ sent["Content-Length"] = str(payload.length)
452
+ if "host" not in names:
453
+ sent["Host"] = target.authority
454
+ if "accept-encoding" not in names:
455
+ sent["Accept-Encoding"] = "gzip, deflate"
456
+ if (
457
+ proxy is not None
458
+ and target.scheme == "http"
459
+ and proxy.authorization
460
+ and "proxy-authorization" not in names
461
+ ):
462
+ sent["Proxy-Authorization"] = proxy.authorization
463
+ return sent
464
+
465
+ def _exchange(
466
+ self,
467
+ connection,
468
+ proxy,
469
+ method,
470
+ target,
471
+ pairs,
472
+ names,
473
+ payload,
474
+ url,
475
+ decode,
476
+ reused,
477
+ ) -> HTTPResponse:
478
+ http = connection.http
479
+ if proxy is not None and target.scheme == "http":
480
+ line = f"http://{target.authority}{target.path}"
481
+ else:
482
+ line = target.path
483
+ try:
484
+ http.request(
485
+ method,
486
+ line,
487
+ body=payload.value,
488
+ headers=self._headers(proxy, target, pairs, names, payload),
489
+ )
490
+ response = http.getresponse()
491
+ except (OSError, _client.HTTPException) as error:
492
+ self._pool.discard(connection)
493
+ if reused and _dropped(error):
494
+ raise _Dropped(_failure(error)) from None
495
+ raise _failure(error) from None
496
+ except BaseException:
497
+ self._pool.discard(connection)
498
+ raise
499
+ headers = response.getheaders()
500
+ body = _Body(self, self._pool, connection, response, response.length)
501
+ return HTTPResponse(
502
+ response.status, response.reason, headers, url, body, decode=decode
503
+ )
504
+
505
+ def _open(self, proxy, target, key, scope, connect_timeout, read_timeout):
506
+ host, port = (proxy.host, proxy.port) if proxy else (target.host, target.port)
507
+ try:
508
+ raw = _socket.create_connection((host, port), timeout=connect_timeout)
509
+ except (OSError, _client.HTTPException) as error:
510
+ raise _CannotConnect(_failure(error)) from None
511
+ sock = raw
512
+ try:
513
+ raw.setsockopt(_socket.IPPROTO_TCP, _socket.TCP_NODELAY, 1)
514
+ if target.scheme == "https":
515
+ if proxy is not None:
516
+ self._tunnel(raw, target, proxy)
517
+ sock = self._context().wrap_socket(raw, server_hostname=target.host)
518
+ sock.settimeout(read_timeout)
519
+ except (OSError, _client.HTTPException) as error:
520
+ raw.close()
521
+ raise _failure(error) from None
522
+ except BaseException:
523
+ raw.close()
524
+ raise
525
+ http = _client.HTTPConnection(target.host, target.port)
526
+ # A connection that was closed stays closed: `http.client` would
527
+ # otherwise reconnect by itself, in plain TCP, outside the TLS session,
528
+ # the proxy and the timeout this request was given.
529
+ http.auto_open = 0
530
+ http.response_class = _Reply
531
+ http.sock = sock
532
+ connection = _pool.Connection(http, key, scope, self._pool.generation)
533
+ self._pool.register(connection)
534
+ return connection
535
+
536
+ @staticmethod
537
+ def _tunnel(sock, target: Target, proxy: ProxyAddress) -> None:
538
+ """Asks the proxy to connect `sock` to the server (RFC 9110 9.3.6)."""
539
+ authority = f"{bracket(target.host)}:{target.port}"
540
+ lines = [f"CONNECT {authority} HTTP/1.1", f"Host: {authority}"]
541
+ if proxy.authorization:
542
+ lines.append(f"Proxy-Authorization: {proxy.authorization}")
543
+ sock.sendall(("\r\n".join(lines) + "\r\n\r\n").encode("ascii"))
544
+ reply = _client.HTTPResponse(sock, method="CONNECT")
545
+ try:
546
+ reply.begin()
547
+ status = reply.status
548
+ finally:
549
+ reply.close()
550
+ if status != 200:
551
+ raise ConnectionRefusedError(
552
+ _errno.ECONNREFUSED, f"the proxy refused CONNECT (HTTP {status})"
553
+ )