Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/httpx/_main.py: 5%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

169 statements  

1from __future__ import annotations 

2 

3import functools 

4import json 

5import sys 

6import typing 

7 

8import click 

9import httpcore 

10import pygments.lexers 

11import pygments.util 

12import rich.console 

13import rich.markup 

14import rich.progress 

15import rich.syntax 

16import rich.table 

17 

18from ._client import Client 

19from ._exceptions import RequestError 

20from ._models import Response 

21from ._status_codes import codes 

22 

23 

24def print_help() -> None: 

25 console = rich.console.Console() 

26 

27 console.print("[bold]HTTPX :butterfly:", justify="center") 

28 console.print() 

29 console.print("A next generation HTTP client.", justify="center") 

30 console.print() 

31 console.print( 

32 "Usage: [bold]httpx[/bold] [cyan]<URL> [OPTIONS][/cyan] ", justify="left" 

33 ) 

34 console.print() 

35 

36 table = rich.table.Table.grid(padding=1, pad_edge=True) 

37 table.add_column("Parameter", no_wrap=True, justify="left", style="bold") 

38 table.add_column("Description") 

39 table.add_row( 

40 "-m, --method [cyan]METHOD", 

41 "Request method, such as GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD.\n" 

42 "[Default: GET, or POST if a request body is included]", 

43 ) 

44 table.add_row( 

45 "-p, --params [cyan]<NAME VALUE> ...", 

46 "Query parameters to include in the request URL.", 

47 ) 

48 table.add_row( 

49 "-c, --content [cyan]TEXT", "Byte content to include in the request body." 

50 ) 

51 table.add_row( 

52 "-d, --data [cyan]<NAME VALUE> ...", "Form data to include in the request body." 

53 ) 

54 table.add_row( 

55 "-f, --files [cyan]<NAME FILENAME> ...", 

56 "Form files to include in the request body.", 

57 ) 

58 table.add_row("-j, --json [cyan]TEXT", "JSON data to include in the request body.") 

59 table.add_row( 

60 "-h, --headers [cyan]<NAME VALUE> ...", 

61 "Include additional HTTP headers in the request.", 

62 ) 

63 table.add_row( 

64 "--cookies [cyan]<NAME VALUE> ...", "Cookies to include in the request." 

65 ) 

66 table.add_row( 

67 "--auth [cyan]<USER PASS>", 

68 "Username and password to include in the request. Specify '-' for the password" 

69 " to use a password prompt. Note that using --verbose/-v will expose" 

70 " the Authorization header, including the password encoding" 

71 " in a trivially reversible format.", 

72 ) 

73 

74 table.add_row( 

75 "--proxy [cyan]URL", 

76 "Send the request via a proxy. Should be the URL giving the proxy address.", 

77 ) 

78 

79 table.add_row( 

80 "--timeout [cyan]FLOAT", 

81 "Timeout value to use for network operations, such as establishing the" 

82 " connection, reading some data, etc... [Default: 5.0]", 

83 ) 

84 

85 table.add_row("--follow-redirects", "Automatically follow redirects.") 

86 table.add_row("--no-verify", "Disable SSL verification.") 

87 table.add_row( 

88 "--http2", "Send the request using HTTP/2, if the remote server supports it." 

89 ) 

90 

91 table.add_row( 

92 "--download [cyan]FILE", 

93 "Save the response content as a file, rather than displaying it.", 

94 ) 

95 

96 table.add_row("-v, --verbose", "Verbose output. Show request as well as response.") 

97 table.add_row("--help", "Show this message and exit.") 

98 console.print(table) 

99 

100 

101def get_lexer_for_response(response: Response) -> str: 

102 content_type = response.headers.get("Content-Type") 

103 if content_type is not None: 

104 mime_type, _, _ = content_type.partition(";") 

105 try: 

106 return typing.cast( 

107 str, pygments.lexers.get_lexer_for_mimetype(mime_type.strip()).name 

108 ) 

109 except pygments.util.ClassNotFound: # pragma: no cover 

110 pass 

111 return "" # pragma: no cover 

112 

113 

114def format_request_headers(request: httpcore.Request, http2: bool = False) -> str: 

115 version = "HTTP/2" if http2 else "HTTP/1.1" 

116 headers = [ 

117 (name.lower() if http2 else name, value) for name, value in request.headers 

118 ] 

119 method = request.method.decode("ascii") 

120 target = request.url.target.decode("ascii") 

121 lines = [f"{method} {target} {version}"] + [ 

122 f"{name.decode('ascii')}: {value.decode('ascii')}" for name, value in headers 

123 ] 

124 return "\n".join(lines) 

125 

126 

127def format_response_headers( 

128 http_version: bytes, 

129 status: int, 

130 reason_phrase: bytes | None, 

131 headers: list[tuple[bytes, bytes]], 

132) -> str: 

133 version = http_version.decode("ascii") 

134 reason = ( 

135 codes.get_reason_phrase(status) 

136 if reason_phrase is None 

137 else reason_phrase.decode("ascii") 

138 ) 

139 lines = [f"{version} {status} {reason}"] + [ 

140 f"{name.decode('ascii')}: {value.decode('ascii')}" for name, value in headers 

141 ] 

142 return "\n".join(lines) 

143 

144 

145def print_request_headers(request: httpcore.Request, http2: bool = False) -> None: 

146 console = rich.console.Console() 

147 http_text = format_request_headers(request, http2=http2) 

148 syntax = rich.syntax.Syntax(http_text, "http", theme="ansi_dark", word_wrap=True) 

149 console.print(syntax) 

150 syntax = rich.syntax.Syntax("", "http", theme="ansi_dark", word_wrap=True) 

151 console.print(syntax) 

152 

153 

154def print_response_headers( 

155 http_version: bytes, 

156 status: int, 

157 reason_phrase: bytes | None, 

158 headers: list[tuple[bytes, bytes]], 

159) -> None: 

160 console = rich.console.Console() 

161 http_text = format_response_headers(http_version, status, reason_phrase, headers) 

162 syntax = rich.syntax.Syntax(http_text, "http", theme="ansi_dark", word_wrap=True) 

163 console.print(syntax) 

164 syntax = rich.syntax.Syntax("", "http", theme="ansi_dark", word_wrap=True) 

165 console.print(syntax) 

166 

167 

168def print_response(response: Response) -> None: 

169 console = rich.console.Console() 

170 lexer_name = get_lexer_for_response(response) 

171 if lexer_name: 

172 if lexer_name.lower() == "json": 

173 try: 

174 data = response.json() 

175 text = json.dumps(data, indent=4) 

176 except ValueError: # pragma: no cover 

177 text = response.text 

178 else: 

179 text = response.text 

180 

181 syntax = rich.syntax.Syntax(text, lexer_name, theme="ansi_dark", word_wrap=True) 

182 console.print(syntax) 

183 else: 

184 console.print(f"<{len(response.content)} bytes of binary data>") 

185 

186 

187_PCTRTT = typing.Tuple[typing.Tuple[str, str], ...] 

188_PCTRTTT = typing.Tuple[_PCTRTT, ...] 

189_PeerCertRetDictType = typing.Dict[str, typing.Union[str, _PCTRTTT, _PCTRTT]] 

190 

191 

192def format_certificate(cert: _PeerCertRetDictType) -> str: # pragma: no cover 

193 lines = [] 

194 for key, value in cert.items(): 

195 if isinstance(value, (list, tuple)): 

196 lines.append(f"* {key}:") 

197 for item in value: 

198 if key in ("subject", "issuer"): 

199 for sub_item in item: 

200 lines.append(f"* {sub_item[0]}: {sub_item[1]!r}") 

201 elif isinstance(item, tuple) and len(item) == 2: 

202 lines.append(f"* {item[0]}: {item[1]!r}") 

203 else: 

204 lines.append(f"* {item!r}") 

205 else: 

206 lines.append(f"* {key}: {value!r}") 

207 return "\n".join(lines) 

208 

209 

210def trace( 

211 name: str, info: typing.Mapping[str, typing.Any], verbose: bool = False 

212) -> None: 

213 console = rich.console.Console() 

214 if name == "connection.connect_tcp.started" and verbose: 

215 host = info["host"] 

216 console.print(f"* Connecting to {host!r}") 

217 elif name == "connection.connect_tcp.complete" and verbose: 

218 stream = info["return_value"] 

219 server_addr = stream.get_extra_info("server_addr") 

220 console.print(f"* Connected to {server_addr[0]!r} on port {server_addr[1]}") 

221 elif name == "connection.start_tls.complete" and verbose: # pragma: no cover 

222 stream = info["return_value"] 

223 ssl_object = stream.get_extra_info("ssl_object") 

224 version = ssl_object.version() 

225 cipher = ssl_object.cipher() 

226 server_cert = ssl_object.getpeercert() 

227 alpn = ssl_object.selected_alpn_protocol() 

228 console.print(f"* SSL established using {version!r} / {cipher[0]!r}") 

229 console.print(f"* Selected ALPN protocol: {alpn!r}") 

230 if server_cert: 

231 console.print("* Server certificate:") 

232 console.print(format_certificate(server_cert)) 

233 elif name == "http11.send_request_headers.started" and verbose: 

234 request = info["request"] 

235 print_request_headers(request, http2=False) 

236 elif name == "http2.send_request_headers.started" and verbose: # pragma: no cover 

237 request = info["request"] 

238 print_request_headers(request, http2=True) 

239 elif name == "http11.receive_response_headers.complete": 

240 http_version, status, reason_phrase, headers = info["return_value"] 

241 print_response_headers(http_version, status, reason_phrase, headers) 

242 elif name == "http2.receive_response_headers.complete": # pragma: no cover 

243 status, headers = info["return_value"] 

244 http_version = b"HTTP/2" 

245 reason_phrase = None 

246 print_response_headers(http_version, status, reason_phrase, headers) 

247 

248 

249def download_response(response: Response, download: typing.BinaryIO) -> None: 

250 console = rich.console.Console() 

251 console.print() 

252 content_length = response.headers.get("Content-Length") 

253 with rich.progress.Progress( 

254 "[progress.description]{task.description}", 

255 "[progress.percentage]{task.percentage:>3.0f}%", 

256 rich.progress.BarColumn(bar_width=None), 

257 rich.progress.DownloadColumn(), 

258 rich.progress.TransferSpeedColumn(), 

259 ) as progress: 

260 description = f"Downloading [bold]{rich.markup.escape(download.name)}" 

261 download_task = progress.add_task( 

262 description, 

263 total=int(content_length or 0), 

264 start=content_length is not None, 

265 ) 

266 for chunk in response.iter_bytes(): 

267 download.write(chunk) 

268 progress.update(download_task, completed=response.num_bytes_downloaded) 

269 

270 

271def validate_json( 

272 ctx: click.Context, 

273 param: click.Option | click.Parameter, 

274 value: typing.Any, 

275) -> typing.Any: 

276 if value is None: 

277 return None 

278 

279 try: 

280 return json.loads(value) 

281 except json.JSONDecodeError: # pragma: no cover 

282 raise click.BadParameter("Not valid JSON") 

283 

284 

285def validate_auth( 

286 ctx: click.Context, 

287 param: click.Option | click.Parameter, 

288 value: typing.Any, 

289) -> typing.Any: 

290 if value == (None, None): 

291 return None 

292 

293 username, password = value 

294 if password == "-": # pragma: no cover 

295 password = click.prompt("Password", hide_input=True) 

296 return (username, password) 

297 

298 

299def handle_help( 

300 ctx: click.Context, 

301 param: click.Option | click.Parameter, 

302 value: typing.Any, 

303) -> None: 

304 if not value or ctx.resilient_parsing: 

305 return 

306 

307 print_help() 

308 ctx.exit() 

309 

310 

311@click.command(add_help_option=False) 

312@click.argument("url", type=str) 

313@click.option( 

314 "--method", 

315 "-m", 

316 "method", 

317 type=str, 

318 help=( 

319 "Request method, such as GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD. " 

320 "[Default: GET, or POST if a request body is included]" 

321 ), 

322) 

323@click.option( 

324 "--params", 

325 "-p", 

326 "params", 

327 type=(str, str), 

328 multiple=True, 

329 help="Query parameters to include in the request URL.", 

330) 

331@click.option( 

332 "--content", 

333 "-c", 

334 "content", 

335 type=str, 

336 help="Byte content to include in the request body.", 

337) 

338@click.option( 

339 "--data", 

340 "-d", 

341 "data", 

342 type=(str, str), 

343 multiple=True, 

344 help="Form data to include in the request body.", 

345) 

346@click.option( 

347 "--files", 

348 "-f", 

349 "files", 

350 type=(str, click.File(mode="rb")), 

351 multiple=True, 

352 help="Form files to include in the request body.", 

353) 

354@click.option( 

355 "--json", 

356 "-j", 

357 "json", 

358 type=str, 

359 callback=validate_json, 

360 help="JSON data to include in the request body.", 

361) 

362@click.option( 

363 "--headers", 

364 "-h", 

365 "headers", 

366 type=(str, str), 

367 multiple=True, 

368 help="Include additional HTTP headers in the request.", 

369) 

370@click.option( 

371 "--cookies", 

372 "cookies", 

373 type=(str, str), 

374 multiple=True, 

375 help="Cookies to include in the request.", 

376) 

377@click.option( 

378 "--auth", 

379 "auth", 

380 type=(str, str), 

381 default=(None, None), 

382 callback=validate_auth, 

383 help=( 

384 "Username and password to include in the request. " 

385 "Specify '-' for the password to use a password prompt. " 

386 "Note that using --verbose/-v will expose the Authorization header, " 

387 "including the password encoding in a trivially reversible format." 

388 ), 

389) 

390@click.option( 

391 "--proxy", 

392 "proxy", 

393 type=str, 

394 default=None, 

395 help="Send the request via a proxy. Should be the URL giving the proxy address.", 

396) 

397@click.option( 

398 "--timeout", 

399 "timeout", 

400 type=float, 

401 default=5.0, 

402 help=( 

403 "Timeout value to use for network operations, such as establishing the " 

404 "connection, reading some data, etc... [Default: 5.0]" 

405 ), 

406) 

407@click.option( 

408 "--follow-redirects", 

409 "follow_redirects", 

410 is_flag=True, 

411 default=False, 

412 help="Automatically follow redirects.", 

413) 

414@click.option( 

415 "--no-verify", 

416 "verify", 

417 is_flag=True, 

418 default=True, 

419 help="Disable SSL verification.", 

420) 

421@click.option( 

422 "--http2", 

423 "http2", 

424 type=bool, 

425 is_flag=True, 

426 default=False, 

427 help="Send the request using HTTP/2, if the remote server supports it.", 

428) 

429@click.option( 

430 "--download", 

431 type=click.File("wb"), 

432 help="Save the response content as a file, rather than displaying it.", 

433) 

434@click.option( 

435 "--verbose", 

436 "-v", 

437 type=bool, 

438 is_flag=True, 

439 default=False, 

440 help="Verbose. Show request as well as response.", 

441) 

442@click.option( 

443 "--help", 

444 is_flag=True, 

445 is_eager=True, 

446 expose_value=False, 

447 callback=handle_help, 

448 help="Show this message and exit.", 

449) 

450def main( 

451 url: str, 

452 method: str, 

453 params: list[tuple[str, str]], 

454 content: str, 

455 data: list[tuple[str, str]], 

456 files: list[tuple[str, click.File]], 

457 json: str, 

458 headers: list[tuple[str, str]], 

459 cookies: list[tuple[str, str]], 

460 auth: tuple[str, str] | None, 

461 proxy: str, 

462 timeout: float, 

463 follow_redirects: bool, 

464 verify: bool, 

465 http2: bool, 

466 download: typing.BinaryIO | None, 

467 verbose: bool, 

468) -> None: 

469 """ 

470 An HTTP command line client. 

471 Sends a request and displays the response. 

472 """ 

473 if not method: 

474 method = "POST" if content or data or files or json else "GET" 

475 

476 try: 

477 with Client( 

478 proxy=proxy, 

479 timeout=timeout, 

480 verify=verify, 

481 http2=http2, 

482 ) as client: 

483 with client.stream( 

484 method, 

485 url, 

486 params=list(params), 

487 content=content, 

488 data=dict(data), 

489 files=files, # type: ignore 

490 json=json, 

491 headers=headers, 

492 cookies=dict(cookies), 

493 auth=auth, 

494 follow_redirects=follow_redirects, 

495 extensions={"trace": functools.partial(trace, verbose=verbose)}, 

496 ) as response: 

497 if download is not None: 

498 download_response(response, download) 

499 else: 

500 response.read() 

501 if response.content: 

502 print_response(response) 

503 

504 except RequestError as exc: 

505 console = rich.console.Console() 

506 console.print(f"[red]{type(exc).__name__}[/red]: {exc}") 

507 sys.exit(1) 

508 

509 sys.exit(0 if response.is_success else 1)