Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/httpcore/_models.py: 28%

148 statements  

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

1from typing import ( 

2 Any, 

3 AsyncIterable, 

4 AsyncIterator, 

5 Iterable, 

6 Iterator, 

7 List, 

8 Mapping, 

9 Optional, 

10 Sequence, 

11 Tuple, 

12 Union, 

13) 

14from urllib.parse import urlparse 

15 

16# Functions for typechecking... 

17 

18 

19HeadersAsSequence = Sequence[Tuple[Union[bytes, str], Union[bytes, str]]] 

20HeadersAsMapping = Mapping[Union[bytes, str], Union[bytes, str]] 

21HeaderTypes = Union[HeadersAsSequence, HeadersAsMapping, None] 

22 

23Extensions = Mapping[str, Any] 

24 

25 

26def enforce_bytes(value: Union[bytes, str], *, name: str) -> bytes: 

27 """ 

28 Any arguments that are ultimately represented as bytes can be specified 

29 either as bytes or as strings. 

30 

31 However we enforce that any string arguments must only contain characters in 

32 the plain ASCII range. chr(0)...chr(127). If you need to use characters 

33 outside that range then be precise, and use a byte-wise argument. 

34 """ 

35 if isinstance(value, str): 

36 try: 

37 return value.encode("ascii") 

38 except UnicodeEncodeError: 

39 raise TypeError(f"{name} strings may not include unicode characters.") 

40 elif isinstance(value, bytes): 

41 return value 

42 

43 seen_type = type(value).__name__ 

44 raise TypeError(f"{name} must be bytes or str, but got {seen_type}.") 

45 

46 

47def enforce_url(value: Union["URL", bytes, str], *, name: str) -> "URL": 

48 """ 

49 Type check for URL parameters. 

50 """ 

51 if isinstance(value, (bytes, str)): 

52 return URL(value) 

53 elif isinstance(value, URL): 

54 return value 

55 

56 seen_type = type(value).__name__ 

57 raise TypeError(f"{name} must be a URL, bytes, or str, but got {seen_type}.") 

58 

59 

60def enforce_headers( 

61 value: Union[HeadersAsMapping, HeadersAsSequence, None] = None, *, name: str 

62) -> List[Tuple[bytes, bytes]]: 

63 """ 

64 Convienence function that ensure all items in request or response headers 

65 are either bytes or strings in the plain ASCII range. 

66 """ 

67 if value is None: 

68 return [] 

69 elif isinstance(value, Mapping): 

70 return [ 

71 ( 

72 enforce_bytes(k, name="header name"), 

73 enforce_bytes(v, name="header value"), 

74 ) 

75 for k, v in value.items() 

76 ] 

77 elif isinstance(value, Sequence): 

78 return [ 

79 ( 

80 enforce_bytes(k, name="header name"), 

81 enforce_bytes(v, name="header value"), 

82 ) 

83 for k, v in value 

84 ] 

85 

86 seen_type = type(value).__name__ 

87 raise TypeError( 

88 f"{name} must be a mapping or sequence of two-tuples, but got {seen_type}." 

89 ) 

90 

91 

92def enforce_stream( 

93 value: Union[bytes, Iterable[bytes], AsyncIterable[bytes], None], *, name: str 

94) -> Union[Iterable[bytes], AsyncIterable[bytes]]: 

95 if value is None: 

96 return ByteStream(b"") 

97 elif isinstance(value, bytes): 

98 return ByteStream(value) 

99 return value 

100 

101 

102# * https://tools.ietf.org/html/rfc3986#section-3.2.3 

103# * https://url.spec.whatwg.org/#url-miscellaneous 

104# * https://url.spec.whatwg.org/#scheme-state 

105DEFAULT_PORTS = { 

106 b"ftp": 21, 

107 b"http": 80, 

108 b"https": 443, 

109 b"ws": 80, 

110 b"wss": 443, 

111} 

112 

113 

114def include_request_headers( 

115 headers: List[Tuple[bytes, bytes]], 

116 *, 

117 url: "URL", 

118 content: Union[None, bytes, Iterable[bytes], AsyncIterable[bytes]], 

119) -> List[Tuple[bytes, bytes]]: 

120 headers_set = set(k.lower() for k, v in headers) 

121 

122 if b"host" not in headers_set: 

123 default_port = DEFAULT_PORTS.get(url.scheme) 

124 if url.port is None or url.port == default_port: 

125 header_value = url.host 

126 else: 

127 header_value = b"%b:%d" % (url.host, url.port) 

128 headers = [(b"Host", header_value)] + headers 

129 

130 if ( 

131 content is not None 

132 and b"content-length" not in headers_set 

133 and b"transfer-encoding" not in headers_set 

134 ): 

135 if isinstance(content, bytes): 

136 content_length = str(len(content)).encode("ascii") 

137 headers += [(b"Content-Length", content_length)] 

138 else: 

139 headers += [(b"Transfer-Encoding", b"chunked")] # pragma: nocover 

140 

141 return headers 

142 

143 

144# Interfaces for byte streams... 

145 

146 

147class ByteStream: 

148 """ 

149 A container for non-streaming content, and that supports both sync and async 

150 stream iteration. 

151 """ 

152 

153 def __init__(self, content: bytes) -> None: 

154 self._content = content 

155 

156 def __iter__(self) -> Iterator[bytes]: 

157 yield self._content 

158 

159 async def __aiter__(self) -> AsyncIterator[bytes]: 

160 yield self._content 

161 

162 def __repr__(self) -> str: 

163 return f"<{self.__class__.__name__} [{len(self._content)} bytes]>" 

164 

165 

166class Origin: 

167 def __init__(self, scheme: bytes, host: bytes, port: int) -> None: 

168 self.scheme = scheme 

169 self.host = host 

170 self.port = port 

171 

172 def __eq__(self, other: Any) -> bool: 

173 return ( 

174 isinstance(other, Origin) 

175 and self.scheme == other.scheme 

176 and self.host == other.host 

177 and self.port == other.port 

178 ) 

179 

180 def __str__(self) -> str: 

181 scheme = self.scheme.decode("ascii") 

182 host = self.host.decode("ascii") 

183 port = str(self.port) 

184 return f"{scheme}://{host}:{port}" 

185 

186 

187class URL: 

188 """ 

189 Represents the URL against which an HTTP request may be made. 

190 

191 The URL may either be specified as a plain string, for convienence: 

192 

193 ```python 

194 url = httpcore.URL("https://www.example.com/") 

195 ``` 

196 

197 Or be constructed with explicitily pre-parsed components: 

198 

199 ```python 

200 url = httpcore.URL(scheme=b'https', host=b'www.example.com', port=None, target=b'/') 

201 ``` 

202 

203 Using this second more explicit style allows integrations that are using 

204 `httpcore` to pass through URLs that have already been parsed in order to use 

205 libraries such as `rfc-3986` rather than relying on the stdlib. It also ensures 

206 that URL parsing is treated identically at both the networking level and at any 

207 higher layers of abstraction. 

208 

209 The four components are important here, as they allow the URL to be precisely 

210 specified in a pre-parsed format. They also allow certain types of request to 

211 be created that could not otherwise be expressed. 

212 

213 For example, an HTTP request to `http://www.example.com/` forwarded via a proxy 

214 at `http://localhost:8080`... 

215 

216 ```python 

217 # Constructs an HTTP request with a complete URL as the target: 

218 # GET https://www.example.com/ HTTP/1.1 

219 url = httpcore.URL( 

220 scheme=b'http', 

221 host=b'localhost', 

222 port=8080, 

223 target=b'https://www.example.com/' 

224 ) 

225 request = httpcore.Request( 

226 method="GET", 

227 url=url 

228 ) 

229 ``` 

230 

231 Another example is constructing an `OPTIONS *` request... 

232 

233 ```python 

234 # Constructs an 'OPTIONS *' HTTP request: 

235 # OPTIONS * HTTP/1.1 

236 url = httpcore.URL(scheme=b'https', host=b'www.example.com', target=b'*') 

237 request = httpcore.Request(method="OPTIONS", url=url) 

238 ``` 

239 

240 This kind of request is not possible to formulate with a URL string, 

241 because the `/` delimiter is always used to demark the target from the 

242 host/port portion of the URL. 

243 

244 For convenience, string-like arguments may be specified either as strings or 

245 as bytes. However, once a request is being issue over-the-wire, the URL 

246 components are always ultimately required to be a bytewise representation. 

247 

248 In order to avoid any ambiguity over character encodings, when strings are used 

249 as arguments, they must be strictly limited to the ASCII range `chr(0)`-`chr(127)`. 

250 If you require a bytewise representation that is outside this range you must 

251 handle the character encoding directly, and pass a bytes instance. 

252 """ 

253 

254 def __init__( 

255 self, 

256 url: Union[bytes, str] = "", 

257 *, 

258 scheme: Union[bytes, str] = b"", 

259 host: Union[bytes, str] = b"", 

260 port: Optional[int] = None, 

261 target: Union[bytes, str] = b"", 

262 ) -> None: 

263 """ 

264 Parameters: 

265 url: The complete URL as a string or bytes. 

266 scheme: The URL scheme as a string or bytes. 

267 Typically either `"http"` or `"https"`. 

268 host: The URL host as a string or bytes. Such as `"www.example.com"`. 

269 port: The port to connect to. Either an integer or `None`. 

270 target: The target of the HTTP request. Such as `"/items?search=red"`. 

271 """ 

272 if url: 

273 parsed = urlparse(enforce_bytes(url, name="url")) 

274 self.scheme = parsed.scheme 

275 self.host = parsed.hostname or b"" 

276 self.port = parsed.port 

277 self.target = (parsed.path or b"/") + ( 

278 b"?" + parsed.query if parsed.query else b"" 

279 ) 

280 else: 

281 self.scheme = enforce_bytes(scheme, name="scheme") 

282 self.host = enforce_bytes(host, name="host") 

283 self.port = port 

284 self.target = enforce_bytes(target, name="target") 

285 

286 @property 

287 def origin(self) -> Origin: 

288 default_port = { 

289 b"http": 80, 

290 b"https": 443, 

291 b"ws": 80, 

292 b"wss": 443, 

293 b"socks5": 1080, 

294 }[self.scheme] 

295 return Origin( 

296 scheme=self.scheme, host=self.host, port=self.port or default_port 

297 ) 

298 

299 def __eq__(self, other: Any) -> bool: 

300 return ( 

301 isinstance(other, URL) 

302 and other.scheme == self.scheme 

303 and other.host == self.host 

304 and other.port == self.port 

305 and other.target == self.target 

306 ) 

307 

308 def __bytes__(self) -> bytes: 

309 if self.port is None: 

310 return b"%b://%b%b" % (self.scheme, self.host, self.target) 

311 return b"%b://%b:%d%b" % (self.scheme, self.host, self.port, self.target) 

312 

313 def __repr__(self) -> str: 

314 return ( 

315 f"{self.__class__.__name__}(scheme={self.scheme!r}, " 

316 f"host={self.host!r}, port={self.port!r}, target={self.target!r})" 

317 ) 

318 

319 

320class Request: 

321 """ 

322 An HTTP request. 

323 """ 

324 

325 def __init__( 

326 self, 

327 method: Union[bytes, str], 

328 url: Union[URL, bytes, str], 

329 *, 

330 headers: HeaderTypes = None, 

331 content: Union[bytes, Iterable[bytes], AsyncIterable[bytes], None] = None, 

332 extensions: Optional[Extensions] = None, 

333 ) -> None: 

334 """ 

335 Parameters: 

336 method: The HTTP request method, either as a string or bytes. 

337 For example: `GET`. 

338 url: The request URL, either as a `URL` instance, or as a string or bytes. 

339 For example: `"https://www.example.com".` 

340 headers: The HTTP request headers. 

341 content: The content of the response body. 

342 extensions: A dictionary of optional extra information included on 

343 the request. Possible keys include `"timeout"`, and `"trace"`. 

344 """ 

345 self.method: bytes = enforce_bytes(method, name="method") 

346 self.url: URL = enforce_url(url, name="url") 

347 self.headers: List[Tuple[bytes, bytes]] = enforce_headers( 

348 headers, name="headers" 

349 ) 

350 self.stream: Union[Iterable[bytes], AsyncIterable[bytes]] = enforce_stream( 

351 content, name="content" 

352 ) 

353 self.extensions = {} if extensions is None else extensions 

354 

355 def __repr__(self) -> str: 

356 return f"<{self.__class__.__name__} [{self.method!r}]>" 

357 

358 

359class Response: 

360 """ 

361 An HTTP response. 

362 """ 

363 

364 def __init__( 

365 self, 

366 status: int, 

367 *, 

368 headers: HeaderTypes = None, 

369 content: Union[bytes, Iterable[bytes], AsyncIterable[bytes], None] = None, 

370 extensions: Optional[Extensions] = None, 

371 ) -> None: 

372 """ 

373 Parameters: 

374 status: The HTTP status code of the response. For example `200`. 

375 headers: The HTTP response headers. 

376 content: The content of the response body. 

377 extensions: A dictionary of optional extra information included on 

378 the responseself.Possible keys include `"http_version"`, 

379 `"reason_phrase"`, and `"network_stream"`. 

380 """ 

381 self.status: int = status 

382 self.headers: List[Tuple[bytes, bytes]] = enforce_headers( 

383 headers, name="headers" 

384 ) 

385 self.stream: Union[Iterable[bytes], AsyncIterable[bytes]] = enforce_stream( 

386 content, name="content" 

387 ) 

388 self.extensions = {} if extensions is None else extensions 

389 

390 self._stream_consumed = False 

391 

392 @property 

393 def content(self) -> bytes: 

394 if not hasattr(self, "_content"): 

395 if isinstance(self.stream, Iterable): 

396 raise RuntimeError( 

397 "Attempted to access 'response.content' on a streaming response. " 

398 "Call 'response.read()' first." 

399 ) 

400 else: 

401 raise RuntimeError( 

402 "Attempted to access 'response.content' on a streaming response. " 

403 "Call 'await response.aread()' first." 

404 ) 

405 return self._content 

406 

407 def __repr__(self) -> str: 

408 return f"<{self.__class__.__name__} [{self.status}]>" 

409 

410 # Sync interface... 

411 

412 def read(self) -> bytes: 

413 if not isinstance(self.stream, Iterable): # pragma: nocover 

414 raise RuntimeError( 

415 "Attempted to read an asynchronous response using 'response.read()'. " 

416 "You should use 'await response.aread()' instead." 

417 ) 

418 if not hasattr(self, "_content"): 

419 self._content = b"".join([part for part in self.iter_stream()]) 

420 return self._content 

421 

422 def iter_stream(self) -> Iterator[bytes]: 

423 if not isinstance(self.stream, Iterable): # pragma: nocover 

424 raise RuntimeError( 

425 "Attempted to stream an asynchronous response using 'for ... in " 

426 "response.iter_stream()'. " 

427 "You should use 'async for ... in response.aiter_stream()' instead." 

428 ) 

429 if self._stream_consumed: 

430 raise RuntimeError( 

431 "Attempted to call 'for ... in response.iter_stream()' more than once." 

432 ) 

433 self._stream_consumed = True 

434 for chunk in self.stream: 

435 yield chunk 

436 

437 def close(self) -> None: 

438 if not isinstance(self.stream, Iterable): # pragma: nocover 

439 raise RuntimeError( 

440 "Attempted to close an asynchronous response using 'response.close()'. " 

441 "You should use 'await response.aclose()' instead." 

442 ) 

443 if hasattr(self.stream, "close"): 

444 self.stream.close() 

445 

446 # Async interface... 

447 

448 async def aread(self) -> bytes: 

449 if not isinstance(self.stream, AsyncIterable): # pragma: nocover 

450 raise RuntimeError( 

451 "Attempted to read an synchronous response using " 

452 "'await response.aread()'. " 

453 "You should use 'response.read()' instead." 

454 ) 

455 if not hasattr(self, "_content"): 

456 self._content = b"".join([part async for part in self.aiter_stream()]) 

457 return self._content 

458 

459 async def aiter_stream(self) -> AsyncIterator[bytes]: 

460 if not isinstance(self.stream, AsyncIterable): # pragma: nocover 

461 raise RuntimeError( 

462 "Attempted to stream an synchronous response using 'async for ... in " 

463 "response.aiter_stream()'. " 

464 "You should use 'for ... in response.iter_stream()' instead." 

465 ) 

466 if self._stream_consumed: 

467 raise RuntimeError( 

468 "Attempted to call 'async for ... in response.aiter_stream()' " 

469 "more than once." 

470 ) 

471 self._stream_consumed = True 

472 async for chunk in self.stream: 

473 yield chunk 

474 

475 async def aclose(self) -> None: 

476 if not isinstance(self.stream, AsyncIterable): # pragma: nocover 

477 raise RuntimeError( 

478 "Attempted to close a synchronous response using " 

479 "'await response.aclose()'. " 

480 "You should use 'response.close()' instead." 

481 ) 

482 if hasattr(self.stream, "aclose"): 

483 await self.stream.aclose()