omlish 0.0.0.dev368__py3-none-any.whl → 0.0.0.dev369__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,795 @@
1
+ # @omlish-lite
2
+ # ruff: noqa: UP006 UP007 UP043 UP045
3
+ # PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
4
+ # --------------------------------------------
5
+ #
6
+ # 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization
7
+ # ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated
8
+ # documentation.
9
+ #
10
+ # 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive,
11
+ # royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative
12
+ # works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License
13
+ # Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
14
+ # 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation; All Rights Reserved" are retained in Python
15
+ # alone or in any derivative version prepared by Licensee.
16
+ #
17
+ # 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and
18
+ # wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in
19
+ # any such work a brief summary of the changes made to Python.
20
+ #
21
+ # 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES,
22
+ # EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY
23
+ # OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY
24
+ # RIGHTS.
25
+ #
26
+ # 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL
27
+ # DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF
28
+ # ADVISED OF THE POSSIBILITY THEREOF.
29
+ #
30
+ # 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
31
+ #
32
+ # 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint
33
+ # venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade
34
+ # name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
35
+ #
36
+ # 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this
37
+ # License Agreement.
38
+ """
39
+ https://github.com/python/cpython/blob/9b335cc8104dd83a5a1343dc649d1f3606682098/Lib/http/client.py
40
+ """
41
+ import collections.abc
42
+ import email.parser
43
+ import enum
44
+ import http
45
+ import io
46
+ import typing as ta
47
+ import urllib.parse
48
+
49
+ from ....lite.check import check
50
+ from .errors import CoroHttpClientErrors
51
+ from .headers import CoroHttpClientHeaders
52
+ from .io import CoroHttpClientIo
53
+ from .response import CoroHttpClientResponse
54
+ from .status import CoroHttpClientStatusLine
55
+ from .validation import CoroHttpClientValidation
56
+
57
+
58
+ ##
59
+
60
+
61
+ class CoroHttpClientConnection(
62
+ CoroHttpClientErrors,
63
+ CoroHttpClientIo,
64
+ ):
65
+ """
66
+ HTTPConnection goes through a number of "states", which define when a client may legally make another request or
67
+ fetch the response for a particular request. This diagram details these state transitions:
68
+
69
+ (null)
70
+ |
71
+ | HTTPConnection()
72
+ v
73
+ Idle
74
+ |
75
+ | put_request()
76
+ v
77
+ Request-started
78
+ |
79
+ | ( put_header() )* end_headers()
80
+ v
81
+ Request-sent
82
+ |______________________________
83
+ | | get_response() raises
84
+ | response = get_response() | ConnectionError
85
+ v v
86
+ Unread-response Idle
87
+ [Response-headers-read]
88
+ |_____________________
89
+ | |
90
+ | response.read() | put_request()
91
+ v v
92
+ Idle Req-started-unread-response
93
+ ______/|
94
+ / |
95
+ response.read() | | ( put_header() )* end_headers()
96
+ v v
97
+ Request-started Req-sent-unread-response
98
+ |
99
+ | response.read()
100
+ v
101
+ Request-sent
102
+
103
+ This diagram presents the following rules:
104
+ -- a second request may not be started until {response-headers-read}
105
+ -- a response [object] cannot be retrieved until {request-sent}
106
+ -- there is no differentiation between an unread response body and a
107
+ partially read response body
108
+
109
+ Logical State _state _response
110
+ ------------- ------- ----------
111
+ Idle IDLE None
112
+ Request-started REQ_STARTED None
113
+ Request-sent REQ_SENT None
114
+ Unread-response IDLE <response_class>
115
+ Req-started-unread-response REQ_STARTED <response_class>
116
+ Req-sent-unread-response REQ_SENT <response_class>
117
+ """
118
+
119
+ _http_version = 11
120
+ _http_version_str = 'HTTP/1.1'
121
+
122
+ http_port: ta.ClassVar[int] = 80
123
+ https_port: ta.ClassVar[int] = 443
124
+
125
+ default_port = http_port
126
+
127
+ class _NOT_SET: # noqa
128
+ def __new__(cls, *args, **kwargs): # noqa
129
+ raise NotImplementedError
130
+
131
+ class _State(enum.Enum):
132
+ IDLE = 'Idle'
133
+ REQ_STARTED = 'Request-started'
134
+ REQ_SENT = 'Request-sent'
135
+
136
+ def __init__(
137
+ self,
138
+ host: str,
139
+ port: ta.Optional[int] = None,
140
+ *,
141
+ timeout: ta.Union[float, ta.Type[_NOT_SET], None] = _NOT_SET,
142
+ source_address: ta.Optional[str] = None,
143
+ block_size: int = 8192,
144
+ auto_open: bool = True,
145
+ ) -> None:
146
+ super().__init__()
147
+
148
+ self._timeout = timeout
149
+ self._source_address = source_address
150
+ self._block_size = block_size
151
+ self._auto_open = auto_open
152
+
153
+ self._connected = False
154
+ self._buffer: ta.List[bytes] = []
155
+ self._response: ta.Optional[CoroHttpClientResponse] = None
156
+ self._state = self._State.IDLE
157
+ self._method: ta.Optional[str] = None
158
+
159
+ self._tunnel_host: ta.Optional[str] = None
160
+ self._tunnel_port: ta.Optional[int] = None
161
+ self._tunnel_headers: ta.Dict[str, str] = {}
162
+ self._raw_proxy_headers: ta.Optional[ta.Sequence[bytes]] = None
163
+
164
+ (self._host, self._port) = self._get_hostport(host, port)
165
+
166
+ CoroHttpClientValidation.validate_host(self._host)
167
+
168
+ #
169
+
170
+ def _get_hostport(self, host: str, port: ta.Optional[int]) -> ta.Tuple[str, int]:
171
+ if port is None:
172
+ i = host.rfind(':')
173
+ j = host.rfind(']') # ipv6 addresses have [...]
174
+ if i > j:
175
+ try:
176
+ port = int(host[i + 1:])
177
+ except ValueError:
178
+ if host[i + 1:] == '': # http://foo.com:/ == http://foo.com/
179
+ port = self.default_port
180
+ else:
181
+ raise self.InvalidUrlError(f"non-numeric port: '{host[i + 1:]}'") from None
182
+ host = host[:i]
183
+ else:
184
+ port = self.default_port
185
+
186
+ if host and host[0] == '[' and host[-1] == ']':
187
+ host = host[1:-1]
188
+
189
+ return (host, port)
190
+
191
+ def _wrap_ipv6(self, ip: bytes) -> bytes:
192
+ if b':' in ip and ip[0] != b'['[0]:
193
+ return b'[' + ip + b']'
194
+ return ip
195
+
196
+ #
197
+
198
+ def set_tunnel(
199
+ self,
200
+ host: str,
201
+ port: ta.Optional[int] = None,
202
+ headers: ta.Optional[ta.Mapping[str, str]] = None,
203
+ ) -> None:
204
+ """
205
+ Set up host and port for HTTP CONNECT tunnelling.
206
+
207
+ In a connection that uses HTTP CONNECT tunnelling, the host passed to the constructor is used as a proxy server
208
+ that relays all communication to the endpoint passed to `set_tunnel`. This done by sending an HTTP CONNECT
209
+ request to the proxy server when the connection is established.
210
+
211
+ This method must be called before the HTTP connection has been established.
212
+
213
+ The headers argument should be a mapping of extra HTTP headers to send with the CONNECT request.
214
+
215
+ As HTTP/1.1 is used for HTTP CONNECT tunnelling request, as per the RFC
216
+ (https://tools.ietf.org/html/rfc7231#section-4.3.6), a HTTP Host: header must be provided, matching the
217
+ authority-form of the request target provided as the destination for the CONNECT request. If a HTTP Host: header
218
+ is not provided via the headers argument, one is generated and transmitted automatically.
219
+ """
220
+
221
+ if self._connected:
222
+ raise RuntimeError("Can't set up tunnel for established connection")
223
+
224
+ self._tunnel_host, self._tunnel_port = self._get_hostport(host, port)
225
+
226
+ if headers:
227
+ self._tunnel_headers = dict(headers)
228
+ else:
229
+ self._tunnel_headers.clear()
230
+
231
+ if not any(header.lower() == 'host' for header in self._tunnel_headers):
232
+ encoded_host = self._tunnel_host.encode('idna').decode('ascii')
233
+ self._tunnel_headers['Host'] = f'{encoded_host}:{self._tunnel_port:d}'
234
+
235
+ def _tunnel(self) -> ta.Generator[CoroHttpClientIo.Io, ta.Optional[bytes], None]:
236
+ connect = b'CONNECT %s:%d %s\r\n' % (
237
+ self._wrap_ipv6(check.not_none(self._tunnel_host).encode('idna')),
238
+ check.not_none(self._tunnel_port),
239
+ self._http_version_str.encode('ascii'),
240
+ )
241
+
242
+ headers = [connect]
243
+ for header, value in self._tunnel_headers.items():
244
+ headers.append(f'{header}: {value}\r\n'.encode('latin-1'))
245
+ headers.append(b'\r\n')
246
+
247
+ # Making a single send() call instead of one per line encourages the host OS to use a more optimal packet size
248
+ # instead of potentially emitting a series of small packets.
249
+ yield from self.send(b''.join(headers))
250
+ del headers
251
+
252
+ try:
253
+ (version, code, message) = (yield from CoroHttpClientStatusLine.read())
254
+ except self.BadStatusLineError: # noqa
255
+ # self._close_conn()
256
+ raise
257
+
258
+ self._raw_proxy_headers = yield from CoroHttpClientHeaders.read_headers()
259
+
260
+ if code != http.HTTPStatus.OK:
261
+ yield from self.close()
262
+ raise OSError(f'Tunnel connection failed: {code} {message.strip()}')
263
+
264
+ def get_proxy_response_headers(self) -> ta.Optional[email.message.Message]:
265
+ """
266
+ Returns a dictionary with the headers of the response received from the proxy server to the CONNECT request sent
267
+ to set the tunnel.
268
+
269
+ If the CONNECT request was not sent, the method returns None.
270
+ """
271
+
272
+ return (
273
+ CoroHttpClientHeaders.parse_header_lines(self._raw_proxy_headers)
274
+ if self._raw_proxy_headers is not None
275
+ else None
276
+ )
277
+
278
+ #
279
+
280
+ def connect(self) -> ta.Generator[CoroHttpClientIo.Io, None, None]:
281
+ """Connect to the host and port specified in __init__."""
282
+
283
+ if self._connected:
284
+ return
285
+
286
+ check.none((yield self.ConnectIo(
287
+ ((self._host, self._port),),
288
+ dict(
289
+ source_address=self._source_address,
290
+ **(dict(timeout=self._timeout) if self._timeout is not self._NOT_SET else {}),
291
+ ),
292
+ )))
293
+
294
+ self._connected = True
295
+
296
+ if self._tunnel_host:
297
+ yield from self._tunnel()
298
+
299
+ #
300
+
301
+ def close(self) -> ta.Generator[CoroHttpClientIo.Io, ta.Optional[bytes], None]:
302
+ """Close the connection to the HTTP server."""
303
+
304
+ self._state = self._State.IDLE
305
+
306
+ try:
307
+ if self._connected:
308
+ yield self.CloseIo() # Close it manually... there may be other refs
309
+ self._connected = False
310
+
311
+ finally:
312
+ response = self._response
313
+ if response:
314
+ self._response = None
315
+ response.close()
316
+
317
+ #
318
+
319
+ @staticmethod
320
+ def _is_text_io(stream: ta.Any) -> bool:
321
+ """Test whether a file-like object is a text or a binary stream."""
322
+
323
+ return isinstance(stream, io.TextIOBase)
324
+
325
+ def send(self, data: ta.Any) -> ta.Generator[CoroHttpClientIo.Io, ta.Optional[bytes], None]:
326
+ """
327
+ Send `data' to the server. ``data`` can be a string object, a bytes object, an array object, a file-like object
328
+ that supports a .read() method, or an iterable object.
329
+ """
330
+
331
+ if not self._connected:
332
+ if self._auto_open:
333
+ yield from self.connect()
334
+ else:
335
+ raise self.NotConnectedError
336
+
337
+ check.state(self._connected)
338
+
339
+ if hasattr(data, 'read'):
340
+ encode = self._is_text_io(data)
341
+ while data_block := data.read(self._block_size):
342
+ if encode:
343
+ data_block = data_block.encode('iso-8859-1')
344
+ check.none((yield self.WriteIo(data_block)))
345
+ return
346
+
347
+ if isinstance(data, (bytes, bytearray)):
348
+ check.none((yield self.WriteIo(data)))
349
+
350
+ elif isinstance(data, collections.abc.Iterable):
351
+ for d in data:
352
+ check.none((yield self.WriteIo(d)))
353
+
354
+ else:
355
+ raise TypeError(f'data should be a bytes-like object or an iterable, got {type(data)!r}') from None
356
+
357
+ def _output(self, s: bytes) -> None:
358
+ """
359
+ Add a line of output to the current request buffer.
360
+
361
+ Assumes that the line does *not* end with \\r\\n.
362
+ """
363
+
364
+ self._buffer.append(s)
365
+
366
+ def _read_readable(self, readable: ta.Union[ta.IO, ta.TextIO]) -> ta.Iterator[bytes]:
367
+ while data := readable.read(self._block_size):
368
+ if isinstance(data, str):
369
+ yield data.encode('iso-8859-1')
370
+ else:
371
+ yield data
372
+
373
+ def _send_output(
374
+ self,
375
+ message_body: ta.Optional[ta.Any] = None,
376
+ encode_chunked: bool = False,
377
+ ) -> ta.Generator[CoroHttpClientIo.Io, ta.Optional[bytes], None]:
378
+ """
379
+ Send the currently buffered request and clear the buffer.
380
+
381
+ Appends an extra \\r\\n to the buffer. A message_body may be specified, to be appended to the request.
382
+ """
383
+
384
+ self._buffer.extend((b'', b''))
385
+ msg = b'\r\n'.join(self._buffer)
386
+ del self._buffer[:]
387
+ yield from self.send(msg)
388
+
389
+ chunks: ta.Iterable[bytes]
390
+ if message_body is not None:
391
+ # Create a consistent interface to message_body
392
+ if hasattr(message_body, 'read'):
393
+ # Let file-like take precedence over byte-like. This is needed to allow the current position of mmap'ed
394
+ # files to be taken into account.
395
+ chunks = self._read_readable(message_body)
396
+
397
+ else:
398
+ try:
399
+ # This is solely to check to see if message_body implements the buffer API. it /would/ be easier to
400
+ # capture if PyObject_CheckBuffer was exposed to Python.
401
+ memoryview(message_body)
402
+
403
+ except TypeError:
404
+ try:
405
+ chunks = iter(message_body)
406
+ except TypeError as e:
407
+ raise TypeError(
408
+ f'message_body should be a bytes-like object or an iterable, got {type(message_body)!r}',
409
+ ) from e
410
+
411
+ else:
412
+ # The object implements the buffer interface and can be passed directly into socket methods
413
+ chunks = (message_body,)
414
+
415
+ for chunk in chunks:
416
+ if not chunk:
417
+ continue
418
+
419
+ if encode_chunked and self._http_version == 11:
420
+ # Chunked encoding
421
+ chunk = f'{len(chunk):X}\r\n'.encode('ascii') + chunk + b'\r\n'
422
+ yield from self.send(chunk)
423
+
424
+ if encode_chunked and self._http_version == 11:
425
+ # End chunked transfer
426
+ yield from self.send(b'0\r\n\r\n')
427
+
428
+ #
429
+
430
+ @staticmethod
431
+ def _strip_ipv6_iface(enc_name: bytes) -> bytes:
432
+ """Remove interface scope from IPv6 address."""
433
+
434
+ enc_name, percent, _ = enc_name.partition(b'%')
435
+ if percent:
436
+ check.state(enc_name.startswith(b'['))
437
+ enc_name += b']'
438
+ return enc_name
439
+
440
+ def put_request(
441
+ self,
442
+ method: str,
443
+ url: str,
444
+ *,
445
+ skip_host: bool = False,
446
+ skip_accept_encoding: bool = False,
447
+ ) -> None:
448
+ """
449
+ Send a request to the server.
450
+
451
+ `method' specifies an HTTP request method, e.g. 'GET'.
452
+ `url' specifies the object being requested, e.g. '/index.html'.
453
+ `skip_host' if True does not add automatically a 'Host:' header
454
+ `skip_accept_encoding' if True does not add automatically an 'Accept-Encoding:' header
455
+ """
456
+
457
+ # If a prior response has been completed, then forget about it.
458
+ if self._response and self._response.is_closed():
459
+ self._response = None
460
+
461
+ # In certain cases, we cannot issue another request on this connection.
462
+ # this occurs when:
463
+ # 1) we are in the process of sending a request. (_CS_REQ_STARTED)
464
+ # 2) a response to a previous request has signalled that it is going to close the connection upon completion.
465
+ # 3) the headers for the previous response have not been read, thus we cannot determine whether point (2) is
466
+ # true. (_CS_REQ_SENT)
467
+ #
468
+ # If there is no prior response, then we can request at will.
469
+ #
470
+ # If point (2) is true, then we will have passed the socket to the response (effectively meaning, "there is no
471
+ # prior response"), and will open a new one when a new request is made.
472
+ #
473
+ # Note: if a prior response exists, then we *can* start a new request. We are not allowed to begin fetching the
474
+ # response to this new request, however, until that prior response is complete.
475
+ #
476
+ if self._state == self._State.IDLE:
477
+ self._state = self._State.REQ_STARTED
478
+ else:
479
+ raise self.CannotSendRequestError(self._state)
480
+
481
+ CoroHttpClientValidation.validate_method(method)
482
+
483
+ # Save the method for use later in the response phase
484
+ self._method = method
485
+
486
+ url = url or '/'
487
+ CoroHttpClientValidation.validate_path(url)
488
+
489
+ request = f'{method} {url} {self._http_version_str}'
490
+
491
+ self._output(self._encode_request(request))
492
+
493
+ if self._http_version == 11:
494
+ # Issue some standard headers for better HTTP/1.1 compliance
495
+
496
+ if not skip_host:
497
+ # This header is issued *only* for HTTP/1.1 connections. more specifically, this means it is only issued
498
+ # when the client uses the new HTTPConnection() class. backwards-compat clients will be using HTTP/1.0
499
+ # and those clients may be issuing this header themselves. we should NOT issue it twice; some web
500
+ # servers (such as Apache) barf when they see two Host: headers
501
+
502
+ # If we need a non-standard port,include it in the header. If the request is going through a proxy, but
503
+ # the host of the actual URL, not the host of the proxy.
504
+ netloc = ''
505
+ if url.startswith('http'):
506
+ netloc = urllib.parse.urlsplit(url).netloc
507
+
508
+ if netloc:
509
+ try:
510
+ netloc_enc = netloc.encode('ascii')
511
+ except UnicodeEncodeError:
512
+ netloc_enc = netloc.encode('idna')
513
+ self.put_header('Host', self._strip_ipv6_iface(netloc_enc))
514
+ else:
515
+ if self._tunnel_host:
516
+ host = self._tunnel_host
517
+ port = self._tunnel_port
518
+ else:
519
+ host = self._host
520
+ port = self._port
521
+
522
+ try:
523
+ host_enc = host.encode('ascii')
524
+ except UnicodeEncodeError:
525
+ host_enc = host.encode('idna')
526
+
527
+ # As per RFC 273, IPv6 address should be wrapped with [] when used as Host header
528
+ host_enc = self._wrap_ipv6(host_enc)
529
+ if ':' in host:
530
+ host_enc = self._strip_ipv6_iface(host_enc)
531
+
532
+ if port == self.default_port:
533
+ self.put_header('Host', host_enc)
534
+ else:
535
+ self.put_header('Host', f"{host_enc.decode('ascii')}:{port}")
536
+
537
+ # NOTE: We are assuming that clients will not attempt to set these headers since *this* library must deal
538
+ # with the consequences. this also means that when the supporting libraries are updated to recognize other
539
+ # forms, then this code should be changed (removed or updated).
540
+
541
+ # We only want a Content-Encoding of "identity" since we don't support encodings such as x-gzip or
542
+ # x-deflate.
543
+ if not skip_accept_encoding:
544
+ self.put_header('Accept-Encoding', 'identity')
545
+
546
+ # We can accept "chunked" Transfer-Encodings, but no others.
547
+ # NOTE: no TE header implies *only* "chunked"
548
+ #self.put_header('TE', 'chunked')
549
+
550
+ # If TE is supplied in the header, then it must appear in a Connection header.
551
+ #self.put_header('Connection', 'TE')
552
+
553
+ else:
554
+ # For HTTP/1.0, the server will assume "not chunked"
555
+ pass
556
+
557
+ def _encode_request(self, request: str) -> bytes:
558
+ # ASCII also helps prevent CVE-2019-9740.
559
+ return request.encode('ascii')
560
+
561
+ #
562
+
563
+ def put_header(self, header: ta.Union[str, bytes], *values: ta.Union[bytes, str, int]) -> None:
564
+ """
565
+ Send a request header line to the server.
566
+
567
+ For example: h.put_header('Accept', 'text/html')
568
+ """
569
+
570
+ if self._state != self._State.REQ_STARTED:
571
+ raise self.CannotSendHeaderError
572
+
573
+ if hasattr(header, 'encode'):
574
+ bh = header.encode('ascii')
575
+ else:
576
+ bh = header
577
+
578
+ CoroHttpClientValidation.validate_header_name(bh)
579
+
580
+ bvs = []
581
+ for one_value in values:
582
+ if hasattr(one_value, 'encode'):
583
+ bv = one_value.encode('latin-1')
584
+ elif isinstance(one_value, int):
585
+ bv = str(one_value).encode('ascii')
586
+ else:
587
+ bv = one_value
588
+
589
+ CoroHttpClientValidation.validate_header_value(bv)
590
+ bvs.append(bv)
591
+
592
+ value = b'\r\n\t'.join(bvs)
593
+ bh = bh + b': ' + value
594
+ self._output(bh)
595
+
596
+ def end_headers(
597
+ self,
598
+ message_body: ta.Optional[ta.Any] = None,
599
+ *,
600
+ encode_chunked: bool = False,
601
+ ) -> ta.Generator[CoroHttpClientIo.Io, ta.Optional[bytes], None]:
602
+ """
603
+ Indicate that the last header line has been sent to the server.
604
+
605
+ This method sends the request to the server. The optional message_body argument can be used to pass a message
606
+ body associated with the request.
607
+ """
608
+
609
+ if self._state == self._State.REQ_STARTED:
610
+ self._state = self._State.REQ_SENT
611
+ else:
612
+ raise self.CannotSendHeaderError
613
+
614
+ yield from self._send_output(message_body, encode_chunked=encode_chunked)
615
+
616
+ #
617
+
618
+ def request(
619
+ self,
620
+ method: str,
621
+ url: str,
622
+ body: ta.Optional[ta.Any] = None,
623
+ headers: ta.Optional[ta.Mapping[str, str]] = None,
624
+ *,
625
+ encode_chunked: bool = False,
626
+ ) -> ta.Generator[CoroHttpClientIo.Io, ta.Optional[bytes], None]:
627
+ """Send a complete request to the server."""
628
+
629
+ yield from self._send_request(method, url, body, dict(headers or {}), encode_chunked)
630
+
631
+ _METHODS_EXPECTING_BODY: ta.ClassVar[ta.Container[str]] = {'PATCH', 'POST', 'PUT'}
632
+
633
+ @classmethod
634
+ def _get_content_length(
635
+ cls,
636
+ body: ta.Optional[ta.Any],
637
+ method: str,
638
+ ) -> ta.Optional[int]:
639
+ """
640
+ Get the content-length based on the body.
641
+
642
+ If the body is None, we set Content-Length: 0 for methods that expect a body (RFC 7230, Section 3.3.2). We also
643
+ set the Content-Length for any method if the body is a str or bytes-like object and not a file.
644
+ """
645
+
646
+ if body is None:
647
+ # Do an explicit check for not None here to distinguish between unset and set but empty
648
+ if method.upper() in cls._METHODS_EXPECTING_BODY:
649
+ return 0
650
+ else:
651
+ return None
652
+
653
+ if hasattr(body, 'read'):
654
+ # File-like object.
655
+ return None
656
+
657
+ try:
658
+ # Does it implement the buffer protocol (bytes, bytearray, array)?
659
+ mv = memoryview(body)
660
+ return mv.nbytes
661
+ except TypeError:
662
+ pass
663
+
664
+ if isinstance(body, str):
665
+ return len(body)
666
+
667
+ return None
668
+
669
+ @staticmethod
670
+ def _encode(data: str, name: str = 'data') -> bytes:
671
+ """Call data.encode("latin-1") but show a better error message."""
672
+
673
+ try:
674
+ return data.encode('latin-1')
675
+
676
+ except UnicodeEncodeError as err:
677
+ raise UnicodeEncodeError(
678
+ err.encoding,
679
+ err.object,
680
+ err.start,
681
+ err.end,
682
+ "%s (%.20r) is not valid Latin-1. Use %s.encode('utf-8') if you want to send it encoded in UTF-8." % ( # noqa
683
+ name.title(),
684
+ data[err.start:err.end],
685
+ name,
686
+ ),
687
+ ) from None
688
+
689
+ def _send_request(
690
+ self,
691
+ method: str,
692
+ url: str,
693
+ body: ta.Optional[ta.Any],
694
+ headers: ta.Mapping[str, str],
695
+ encode_chunked: bool,
696
+ ) -> ta.Generator[CoroHttpClientIo.Io, ta.Optional[bytes], None]:
697
+ # Honor explicitly requested Host: and Accept-Encoding: headers.
698
+ header_names = frozenset(k.lower() for k in headers)
699
+ skips = {}
700
+ if 'host' in header_names:
701
+ skips['skip_host'] = True
702
+ if 'accept-encoding' in header_names:
703
+ skips['skip_accept_encoding'] = True
704
+
705
+ self.put_request(method, url, **skips)
706
+
707
+ # Chunked encoding will happen if HTTP/1.1 is used and either the caller passes encode_chunked=True or the
708
+ # following conditions hold:
709
+ # 1) Content-Length has not been explicitly set
710
+ # 2) The body is a file or iterable, but not a str or bytes-like
711
+ # 3) Transfer-Encoding has NOT been explicitly set by the caller
712
+
713
+ if 'content-length' not in header_names:
714
+ # Only chunk body if not explicitly set for backwards compatibility, assuming the client code is already
715
+ # handling the chunking
716
+ if 'transfer-encoding' not in header_names:
717
+ # If Content-Length cannot be automatically determined, fall back to chunked encoding
718
+ encode_chunked = False
719
+ content_length = self._get_content_length(body, method)
720
+ if content_length is None:
721
+ if body is not None:
722
+ encode_chunked = True
723
+ self.put_header('Transfer-Encoding', 'chunked')
724
+ else:
725
+ self.put_header('Content-Length', str(content_length))
726
+ else:
727
+ encode_chunked = False
728
+
729
+ for hdr, value in headers.items():
730
+ self.put_header(hdr, value)
731
+
732
+ if isinstance(body, str):
733
+ # RFC 2616 Section 3.7.1 says that text default has a default charset of iso-8859-1.
734
+ body = self._encode(body, 'body')
735
+
736
+ yield from self.end_headers(body, encode_chunked=encode_chunked)
737
+
738
+ #
739
+
740
+ def _new_response(self) -> CoroHttpClientResponse:
741
+ return CoroHttpClientResponse(check.not_none(self._method))
742
+
743
+ def get_response(self) -> ta.Generator[CoroHttpClientIo.Io, ta.Optional[bytes], CoroHttpClientResponse]:
744
+ """
745
+ Get the response from the server.
746
+
747
+ If the HTTPConnection is in the correct state, returns an instance of HttpResponse or of whatever object is
748
+ returned by the response_class variable.
749
+
750
+ If a request has not been sent or if a previous response has not be handled, ResponseNotReady is raised. If the
751
+ HTTP response indicates that the connection should be closed, then it will be closed before the response is
752
+ returned. When the connection is closed, the underlying socket is closed.
753
+ """
754
+
755
+ # If a prior response has been completed, then forget about it.
756
+ if self._response and self._response.is_closed():
757
+ self._response = None
758
+
759
+ # If a prior response exists, then it must be completed (otherwise, we cannot read this response's header to
760
+ # determine the connection-close behavior).
761
+ #
762
+ # NOTE: If a prior response existed, but was connection-close, then the socket and response were made
763
+ # independent of this HTTPConnection object since a new request requires that we open a whole new connection.
764
+ #
765
+ # This means the prior response had one of two states:
766
+ # 1) will_close: this connection was reset and the prior socket and response operate independently
767
+ # 2) persistent: the response was retained and we await its is_closed() status to become true.
768
+ if self._state != self._State.REQ_SENT or self._response:
769
+ raise self.ResponseNotReadyError(self._state)
770
+
771
+ resp = self._new_response()
772
+ resp_state = resp._state # noqa
773
+
774
+ try:
775
+ try:
776
+ yield from resp._begin() # noqa
777
+ except ConnectionError:
778
+ yield from self.close()
779
+ raise
780
+
781
+ check.state(hasattr(resp_state, 'will_close'))
782
+ self._state = self._State.IDLE
783
+
784
+ if resp_state.will_close:
785
+ # This effectively passes the connection to the response
786
+ yield from self.close()
787
+ else:
788
+ # Remember this, so we can tell when it is complete
789
+ self._response = resp
790
+
791
+ return resp
792
+
793
+ except: # noqa
794
+ resp.close()
795
+ raise