Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/httpcore/_sync/connection.py: 26%

113 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-26 06:12 +0000

1import itertools 

2import ssl 

3from types import TracebackType 

4from typing import Iterator, Optional, Type 

5 

6from .._exceptions import ConnectError, ConnectionNotAvailable, ConnectTimeout 

7from .._models import Origin, Request, Response 

8from .._ssl import default_ssl_context 

9from .._synchronization import Lock 

10from .._trace import Trace 

11from ..backends.sync import SyncBackend 

12from ..backends.base import NetworkBackend, NetworkStream 

13from .http11 import HTTP11Connection 

14from .interfaces import ConnectionInterface 

15 

16RETRIES_BACKOFF_FACTOR = 0.5 # 0s, 0.5s, 1s, 2s, 4s, etc. 

17 

18 

19def exponential_backoff(factor: float) -> Iterator[float]: 

20 yield 0 

21 for n in itertools.count(2): 

22 yield factor * (2 ** (n - 2)) 

23 

24 

25class HTTPConnection(ConnectionInterface): 

26 def __init__( 

27 self, 

28 origin: Origin, 

29 ssl_context: Optional[ssl.SSLContext] = None, 

30 keepalive_expiry: Optional[float] = None, 

31 http1: bool = True, 

32 http2: bool = False, 

33 retries: int = 0, 

34 local_address: Optional[str] = None, 

35 uds: Optional[str] = None, 

36 network_backend: Optional[NetworkBackend] = None, 

37 ) -> None: 

38 self._origin = origin 

39 self._ssl_context = ssl_context 

40 self._keepalive_expiry = keepalive_expiry 

41 self._http1 = http1 

42 self._http2 = http2 

43 self._retries = retries 

44 self._local_address = local_address 

45 self._uds = uds 

46 

47 self._network_backend: NetworkBackend = ( 

48 SyncBackend() if network_backend is None else network_backend 

49 ) 

50 self._connection: Optional[ConnectionInterface] = None 

51 self._connect_failed: bool = False 

52 self._request_lock = Lock() 

53 

54 def handle_request(self, request: Request) -> Response: 

55 if not self.can_handle_request(request.url.origin): 

56 raise RuntimeError( 

57 f"Attempted to send request to {request.url.origin} on connection to {self._origin}" 

58 ) 

59 

60 with self._request_lock: 

61 if self._connection is None: 

62 try: 

63 stream = self._connect(request) 

64 

65 ssl_object = stream.get_extra_info("ssl_object") 

66 http2_negotiated = ( 

67 ssl_object is not None 

68 and ssl_object.selected_alpn_protocol() == "h2" 

69 ) 

70 if http2_negotiated or (self._http2 and not self._http1): 

71 from .http2 import HTTP2Connection 

72 

73 self._connection = HTTP2Connection( 

74 origin=self._origin, 

75 stream=stream, 

76 keepalive_expiry=self._keepalive_expiry, 

77 ) 

78 else: 

79 self._connection = HTTP11Connection( 

80 origin=self._origin, 

81 stream=stream, 

82 keepalive_expiry=self._keepalive_expiry, 

83 ) 

84 except Exception as exc: 

85 self._connect_failed = True 

86 raise exc 

87 elif not self._connection.is_available(): 

88 raise ConnectionNotAvailable() 

89 

90 return self._connection.handle_request(request) 

91 

92 def _connect(self, request: Request) -> NetworkStream: 

93 timeouts = request.extensions.get("timeout", {}) 

94 timeout = timeouts.get("connect", None) 

95 

96 retries_left = self._retries 

97 delays = exponential_backoff(factor=RETRIES_BACKOFF_FACTOR) 

98 

99 while True: 

100 try: 

101 if self._uds is None: 

102 kwargs = { 

103 "host": self._origin.host.decode("ascii"), 

104 "port": self._origin.port, 

105 "local_address": self._local_address, 

106 "timeout": timeout, 

107 } 

108 with Trace( 

109 "connection.connect_tcp", request, kwargs 

110 ) as trace: 

111 stream = self._network_backend.connect_tcp(**kwargs) 

112 trace.return_value = stream 

113 else: 

114 kwargs = { 

115 "path": self._uds, 

116 "timeout": timeout, 

117 } 

118 with Trace( 

119 "connection.connect_unix_socket", request, kwargs 

120 ) as trace: 

121 stream = self._network_backend.connect_unix_socket( 

122 **kwargs 

123 ) 

124 trace.return_value = stream 

125 except (ConnectError, ConnectTimeout): 

126 if retries_left <= 0: 

127 raise 

128 retries_left -= 1 

129 delay = next(delays) 

130 # TRACE 'retry' 

131 self._network_backend.sleep(delay) 

132 else: 

133 break 

134 

135 if self._origin.scheme == b"https": 

136 ssl_context = ( 

137 default_ssl_context() 

138 if self._ssl_context is None 

139 else self._ssl_context 

140 ) 

141 alpn_protocols = ["http/1.1", "h2"] if self._http2 else ["http/1.1"] 

142 ssl_context.set_alpn_protocols(alpn_protocols) 

143 

144 kwargs = { 

145 "ssl_context": ssl_context, 

146 "server_hostname": self._origin.host.decode("ascii"), 

147 "timeout": timeout, 

148 } 

149 with Trace("connection.start_tls", request, kwargs) as trace: 

150 stream = stream.start_tls(**kwargs) 

151 trace.return_value = stream 

152 return stream 

153 

154 def can_handle_request(self, origin: Origin) -> bool: 

155 return origin == self._origin 

156 

157 def close(self) -> None: 

158 if self._connection is not None: 

159 self._connection.close() 

160 

161 def is_available(self) -> bool: 

162 if self._connection is None: 

163 # If HTTP/2 support is enabled, and the resulting connection could 

164 # end up as HTTP/2 then we should indicate the connection as being 

165 # available to service multiple requests. 

166 return ( 

167 self._http2 

168 and (self._origin.scheme == b"https" or not self._http1) 

169 and not self._connect_failed 

170 ) 

171 return self._connection.is_available() 

172 

173 def has_expired(self) -> bool: 

174 if self._connection is None: 

175 return self._connect_failed 

176 return self._connection.has_expired() 

177 

178 def is_idle(self) -> bool: 

179 if self._connection is None: 

180 return self._connect_failed 

181 return self._connection.is_idle() 

182 

183 def is_closed(self) -> bool: 

184 if self._connection is None: 

185 return self._connect_failed 

186 return self._connection.is_closed() 

187 

188 def info(self) -> str: 

189 if self._connection is None: 

190 return "CONNECTION FAILED" if self._connect_failed else "CONNECTING" 

191 return self._connection.info() 

192 

193 def __repr__(self) -> str: 

194 return f"<{self.__class__.__name__} [{self.info()}]>" 

195 

196 # These context managers are not used in the standard flow, but are 

197 # useful for testing or working with connection instances directly. 

198 

199 def __enter__(self) -> "HTTPConnection": 

200 return self 

201 

202 def __exit__( 

203 self, 

204 exc_type: Optional[Type[BaseException]] = None, 

205 exc_value: Optional[BaseException] = None, 

206 traceback: Optional[TracebackType] = None, 

207 ) -> None: 

208 self.close()