Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/httpx/_client.py: 26%

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

527 statements  

1from __future__ import annotations 

2 

3import datetime 

4import enum 

5import logging 

6import time 

7import typing 

8import warnings 

9from contextlib import asynccontextmanager, contextmanager 

10from types import TracebackType 

11 

12from .__version__ import __version__ 

13from ._auth import Auth, BasicAuth, FunctionAuth 

14from ._config import ( 

15 DEFAULT_LIMITS, 

16 DEFAULT_MAX_REDIRECTS, 

17 DEFAULT_TIMEOUT_CONFIG, 

18 Limits, 

19 Proxy, 

20 Timeout, 

21) 

22from ._decoders import SUPPORTED_DECODERS 

23from ._exceptions import ( 

24 InvalidURL, 

25 RemoteProtocolError, 

26 TooManyRedirects, 

27 request_context, 

28) 

29from ._models import Cookies, Headers, Request, Response 

30from ._status_codes import codes 

31from ._transports.base import AsyncBaseTransport, BaseTransport 

32from ._transports.default import AsyncHTTPTransport, HTTPTransport 

33from ._types import ( 

34 AsyncByteStream, 

35 AuthTypes, 

36 CertTypes, 

37 CookieTypes, 

38 HeaderTypes, 

39 ProxyTypes, 

40 QueryParamTypes, 

41 RequestContent, 

42 RequestData, 

43 RequestExtensions, 

44 RequestFiles, 

45 SyncByteStream, 

46 TimeoutTypes, 

47) 

48from ._urls import URL, QueryParams 

49from ._utils import URLPattern, get_environment_proxies 

50 

51if typing.TYPE_CHECKING: 

52 import ssl # pragma: no cover 

53 

54__all__ = ["USE_CLIENT_DEFAULT", "AsyncClient", "Client"] 

55 

56# The type annotation for @classmethod and context managers here follows PEP 484 

57# https://www.python.org/dev/peps/pep-0484/#annotating-instance-and-class-methods 

58T = typing.TypeVar("T", bound="Client") 

59U = typing.TypeVar("U", bound="AsyncClient") 

60 

61 

62def _is_https_redirect(url: URL, location: URL) -> bool: 

63 """ 

64 Return 'True' if 'location' is a HTTPS upgrade of 'url' 

65 """ 

66 if url.host != location.host: 

67 return False 

68 

69 return ( 

70 url.scheme == "http" 

71 and _port_or_default(url) == 80 

72 and location.scheme == "https" 

73 and _port_or_default(location) == 443 

74 ) 

75 

76 

77def _port_or_default(url: URL) -> int | None: 

78 if url.port is not None: 

79 return url.port 

80 return {"http": 80, "https": 443}.get(url.scheme) 

81 

82 

83def _same_origin(url: URL, other: URL) -> bool: 

84 """ 

85 Return 'True' if the given URLs share the same origin. 

86 """ 

87 return ( 

88 url.scheme == other.scheme 

89 and url.host == other.host 

90 and _port_or_default(url) == _port_or_default(other) 

91 ) 

92 

93 

94class UseClientDefault: 

95 """ 

96 For some parameters such as `auth=...` and `timeout=...` we need to be able 

97 to indicate the default "unset" state, in a way that is distinctly different 

98 to using `None`. 

99 

100 The default "unset" state indicates that whatever default is set on the 

101 client should be used. This is different to setting `None`, which 

102 explicitly disables the parameter, possibly overriding a client default. 

103 

104 For example we use `timeout=USE_CLIENT_DEFAULT` in the `request()` signature. 

105 Omitting the `timeout` parameter will send a request using whatever default 

106 timeout has been configured on the client. Including `timeout=None` will 

107 ensure no timeout is used. 

108 

109 Note that user code shouldn't need to use the `USE_CLIENT_DEFAULT` constant, 

110 but it is used internally when a parameter is not included. 

111 """ 

112 

113 

114USE_CLIENT_DEFAULT = UseClientDefault() 

115 

116 

117logger = logging.getLogger("httpx") 

118 

119USER_AGENT = f"python-httpx/{__version__}" 

120ACCEPT_ENCODING = ", ".join( 

121 [key for key in SUPPORTED_DECODERS.keys() if key != "identity"] 

122) 

123 

124 

125class ClientState(enum.Enum): 

126 # UNOPENED: 

127 # The client has been instantiated, but has not been used to send a request, 

128 # or been opened by entering the context of a `with` block. 

129 UNOPENED = 1 

130 # OPENED: 

131 # The client has either sent a request, or is within a `with` block. 

132 OPENED = 2 

133 # CLOSED: 

134 # The client has either exited the `with` block, or `close()` has 

135 # been called explicitly. 

136 CLOSED = 3 

137 

138 

139class BoundSyncStream(SyncByteStream): 

140 """ 

141 A byte stream that is bound to a given response instance, and that 

142 ensures the `response.elapsed` is set once the response is closed. 

143 """ 

144 

145 def __init__( 

146 self, stream: SyncByteStream, response: Response, start: float 

147 ) -> None: 

148 self._stream = stream 

149 self._response = response 

150 self._start = start 

151 

152 def __iter__(self) -> typing.Iterator[bytes]: 

153 for chunk in self._stream: 

154 yield chunk 

155 

156 def close(self) -> None: 

157 elapsed = time.perf_counter() - self._start 

158 self._response.elapsed = datetime.timedelta(seconds=elapsed) 

159 self._stream.close() 

160 

161 

162class BoundAsyncStream(AsyncByteStream): 

163 """ 

164 An async byte stream that is bound to a given response instance, and that 

165 ensures the `response.elapsed` is set once the response is closed. 

166 """ 

167 

168 def __init__( 

169 self, stream: AsyncByteStream, response: Response, start: float 

170 ) -> None: 

171 self._stream = stream 

172 self._response = response 

173 self._start = start 

174 

175 async def __aiter__(self) -> typing.AsyncIterator[bytes]: 

176 async for chunk in self._stream: 

177 yield chunk 

178 

179 async def aclose(self) -> None: 

180 elapsed = time.perf_counter() - self._start 

181 self._response.elapsed = datetime.timedelta(seconds=elapsed) 

182 await self._stream.aclose() 

183 

184 

185EventHook = typing.Callable[..., typing.Any] 

186 

187 

188class BaseClient: 

189 def __init__( 

190 self, 

191 *, 

192 auth: AuthTypes | None = None, 

193 params: QueryParamTypes | None = None, 

194 headers: HeaderTypes | None = None, 

195 cookies: CookieTypes | None = None, 

196 timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, 

197 follow_redirects: bool = False, 

198 max_redirects: int = DEFAULT_MAX_REDIRECTS, 

199 event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, 

200 base_url: URL | str = "", 

201 trust_env: bool = True, 

202 default_encoding: str | typing.Callable[[bytes], str] = "utf-8", 

203 ) -> None: 

204 event_hooks = {} if event_hooks is None else event_hooks 

205 

206 self._base_url = self._enforce_trailing_slash(URL(base_url)) 

207 

208 self._auth = self._build_auth(auth) 

209 self._params = QueryParams(params) 

210 self.headers = Headers(headers) 

211 self._cookies = Cookies(cookies) 

212 self._timeout = Timeout(timeout) 

213 self.follow_redirects = follow_redirects 

214 self.max_redirects = max_redirects 

215 self._event_hooks = { 

216 "request": list(event_hooks.get("request", [])), 

217 "response": list(event_hooks.get("response", [])), 

218 } 

219 self._trust_env = trust_env 

220 self._default_encoding = default_encoding 

221 self._state = ClientState.UNOPENED 

222 

223 @property 

224 def is_closed(self) -> bool: 

225 """ 

226 Check if the client being closed 

227 """ 

228 return self._state == ClientState.CLOSED 

229 

230 @property 

231 def trust_env(self) -> bool: 

232 return self._trust_env 

233 

234 def _enforce_trailing_slash(self, url: URL) -> URL: 

235 if url.raw_path.endswith(b"/"): 

236 return url 

237 return url.copy_with(raw_path=url.raw_path + b"/") 

238 

239 def _get_proxy_map( 

240 self, proxy: ProxyTypes | None, allow_env_proxies: bool 

241 ) -> dict[str, Proxy | None]: 

242 if proxy is None: 

243 if allow_env_proxies: 

244 return { 

245 key: None if url is None else Proxy(url=url) 

246 for key, url in get_environment_proxies().items() 

247 } 

248 return {} 

249 else: 

250 proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy 

251 return {"all://": proxy} 

252 

253 @property 

254 def timeout(self) -> Timeout: 

255 return self._timeout 

256 

257 @timeout.setter 

258 def timeout(self, timeout: TimeoutTypes) -> None: 

259 self._timeout = Timeout(timeout) 

260 

261 @property 

262 def event_hooks(self) -> dict[str, list[EventHook]]: 

263 return self._event_hooks 

264 

265 @event_hooks.setter 

266 def event_hooks(self, event_hooks: dict[str, list[EventHook]]) -> None: 

267 self._event_hooks = { 

268 "request": list(event_hooks.get("request", [])), 

269 "response": list(event_hooks.get("response", [])), 

270 } 

271 

272 @property 

273 def auth(self) -> Auth | None: 

274 """ 

275 Authentication class used when none is passed at the request-level. 

276 

277 See also [Authentication][0]. 

278 

279 [0]: /quickstart/#authentication 

280 """ 

281 return self._auth 

282 

283 @auth.setter 

284 def auth(self, auth: AuthTypes) -> None: 

285 self._auth = self._build_auth(auth) 

286 

287 @property 

288 def base_url(self) -> URL: 

289 """ 

290 Base URL to use when sending requests with relative URLs. 

291 """ 

292 return self._base_url 

293 

294 @base_url.setter 

295 def base_url(self, url: URL | str) -> None: 

296 self._base_url = self._enforce_trailing_slash(URL(url)) 

297 

298 @property 

299 def headers(self) -> Headers: 

300 """ 

301 HTTP headers to include when sending requests. 

302 """ 

303 return self._headers 

304 

305 @headers.setter 

306 def headers(self, headers: HeaderTypes) -> None: 

307 client_headers = Headers( 

308 { 

309 b"Accept": b"*/*", 

310 b"Accept-Encoding": ACCEPT_ENCODING.encode("ascii"), 

311 b"Connection": b"keep-alive", 

312 b"User-Agent": USER_AGENT.encode("ascii"), 

313 } 

314 ) 

315 client_headers.update(headers) 

316 self._headers = client_headers 

317 

318 @property 

319 def cookies(self) -> Cookies: 

320 """ 

321 Cookie values to include when sending requests. 

322 """ 

323 return self._cookies 

324 

325 @cookies.setter 

326 def cookies(self, cookies: CookieTypes) -> None: 

327 self._cookies = Cookies(cookies) 

328 

329 @property 

330 def params(self) -> QueryParams: 

331 """ 

332 Query parameters to include in the URL when sending requests. 

333 """ 

334 return self._params 

335 

336 @params.setter 

337 def params(self, params: QueryParamTypes) -> None: 

338 self._params = QueryParams(params) 

339 

340 def build_request( 

341 self, 

342 method: str, 

343 url: URL | str, 

344 *, 

345 content: RequestContent | None = None, 

346 data: RequestData | None = None, 

347 files: RequestFiles | None = None, 

348 json: typing.Any | None = None, 

349 params: QueryParamTypes | None = None, 

350 headers: HeaderTypes | None = None, 

351 cookies: CookieTypes | None = None, 

352 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

353 extensions: RequestExtensions | None = None, 

354 ) -> Request: 

355 """ 

356 Build and return a request instance. 

357 

358 * The `params`, `headers` and `cookies` arguments 

359 are merged with any values set on the client. 

360 * The `url` argument is merged with any `base_url` set on the client. 

361 

362 See also: [Request instances][0] 

363 

364 [0]: /advanced/clients/#request-instances 

365 """ 

366 url = self._merge_url(url) 

367 headers = self._merge_headers(headers) 

368 cookies = self._merge_cookies(cookies) 

369 params = self._merge_queryparams(params) 

370 extensions = {} if extensions is None else extensions 

371 if "timeout" not in extensions: 

372 timeout = ( 

373 self.timeout 

374 if isinstance(timeout, UseClientDefault) 

375 else Timeout(timeout) 

376 ) 

377 extensions = dict(**extensions, timeout=timeout.as_dict()) 

378 return Request( 

379 method, 

380 url, 

381 content=content, 

382 data=data, 

383 files=files, 

384 json=json, 

385 params=params, 

386 headers=headers, 

387 cookies=cookies, 

388 extensions=extensions, 

389 ) 

390 

391 def _merge_url(self, url: URL | str) -> URL: 

392 """ 

393 Merge a URL argument together with any 'base_url' on the client, 

394 to create the URL used for the outgoing request. 

395 """ 

396 merge_url = URL(url) 

397 if merge_url.is_relative_url: 

398 # To merge URLs we always append to the base URL. To get this 

399 # behaviour correct we always ensure the base URL ends in a '/' 

400 # separator, and strip any leading '/' from the merge URL. 

401 # 

402 # So, eg... 

403 # 

404 # >>> client = Client(base_url="https://www.example.com/subpath") 

405 # >>> client.base_url 

406 # URL('https://www.example.com/subpath/') 

407 # >>> client.build_request("GET", "/path").url 

408 # URL('https://www.example.com/subpath/path') 

409 merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/") 

410 return self.base_url.copy_with(raw_path=merge_raw_path) 

411 return merge_url 

412 

413 def _merge_cookies(self, cookies: CookieTypes | None = None) -> CookieTypes | None: 

414 """ 

415 Merge a cookies argument together with any cookies on the client, 

416 to create the cookies used for the outgoing request. 

417 """ 

418 if cookies or self.cookies: 

419 merged_cookies = Cookies(self.cookies) 

420 merged_cookies.update(cookies) 

421 return merged_cookies 

422 return cookies 

423 

424 def _merge_headers(self, headers: HeaderTypes | None = None) -> HeaderTypes | None: 

425 """ 

426 Merge a headers argument together with any headers on the client, 

427 to create the headers used for the outgoing request. 

428 """ 

429 merged_headers = Headers(self.headers) 

430 merged_headers.update(headers) 

431 return merged_headers 

432 

433 def _merge_queryparams( 

434 self, params: QueryParamTypes | None = None 

435 ) -> QueryParamTypes | None: 

436 """ 

437 Merge a queryparams argument together with any queryparams on the client, 

438 to create the queryparams used for the outgoing request. 

439 """ 

440 if params or self.params: 

441 merged_queryparams = QueryParams(self.params) 

442 return merged_queryparams.merge(params) 

443 return params 

444 

445 def _build_auth(self, auth: AuthTypes | None) -> Auth | None: 

446 if auth is None: 

447 return None 

448 elif isinstance(auth, tuple): 

449 return BasicAuth(username=auth[0], password=auth[1]) 

450 elif isinstance(auth, Auth): 

451 return auth 

452 elif callable(auth): 

453 return FunctionAuth(func=auth) 

454 else: 

455 raise TypeError(f'Invalid "auth" argument: {auth!r}') 

456 

457 def _build_request_auth( 

458 self, 

459 request: Request, 

460 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, 

461 ) -> Auth: 

462 auth = ( 

463 self._auth if isinstance(auth, UseClientDefault) else self._build_auth(auth) 

464 ) 

465 

466 if auth is not None: 

467 return auth 

468 

469 username, password = request.url.username, request.url.password 

470 if username or password: 

471 return BasicAuth(username=username, password=password) 

472 

473 return Auth() 

474 

475 def _build_redirect_request(self, request: Request, response: Response) -> Request: 

476 """ 

477 Given a request and a redirect response, return a new request that 

478 should be used to effect the redirect. 

479 """ 

480 method = self._redirect_method(request, response) 

481 url = self._redirect_url(request, response) 

482 headers = self._redirect_headers(request, url, method) 

483 stream = self._redirect_stream(request, method) 

484 cookies = Cookies(self.cookies) 

485 return Request( 

486 method=method, 

487 url=url, 

488 headers=headers, 

489 cookies=cookies, 

490 stream=stream, 

491 extensions=request.extensions, 

492 ) 

493 

494 def _redirect_method(self, request: Request, response: Response) -> str: 

495 """ 

496 When being redirected we may want to change the method of the request 

497 based on certain specs or browser behavior. 

498 """ 

499 method = request.method 

500 

501 # https://tools.ietf.org/html/rfc7231#section-6.4.4 

502 if response.status_code == codes.SEE_OTHER and method != "HEAD": 

503 method = "GET" 

504 

505 # Do what the browsers do, despite standards... 

506 # Turn 302s into GETs. 

507 if response.status_code == codes.FOUND and method != "HEAD": 

508 method = "GET" 

509 

510 # If a POST is responded to with a 301, turn it into a GET. 

511 # This bizarre behaviour is explained in 'requests' issue 1704. 

512 if response.status_code == codes.MOVED_PERMANENTLY and method == "POST": 

513 method = "GET" 

514 

515 return method 

516 

517 def _redirect_url(self, request: Request, response: Response) -> URL: 

518 """ 

519 Return the URL for the redirect to follow. 

520 """ 

521 location = response.headers["Location"] 

522 

523 try: 

524 url = URL(location) 

525 except InvalidURL as exc: 

526 raise RemoteProtocolError( 

527 f"Invalid URL in location header: {exc}.", request=request 

528 ) from None 

529 

530 # Handle malformed 'Location' headers that are "absolute" form, have no host. 

531 # See: https://github.com/encode/httpx/issues/771 

532 if url.scheme and not url.host: 

533 url = url.copy_with(host=request.url.host) 

534 

535 # Facilitate relative 'Location' headers, as allowed by RFC 7231. 

536 # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource') 

537 if url.is_relative_url: 

538 url = request.url.join(url) 

539 

540 # Attach previous fragment if needed (RFC 7231 7.1.2) 

541 if request.url.fragment and not url.fragment: 

542 url = url.copy_with(fragment=request.url.fragment) 

543 

544 return url 

545 

546 def _redirect_headers(self, request: Request, url: URL, method: str) -> Headers: 

547 """ 

548 Return the headers that should be used for the redirect request. 

549 """ 

550 headers = Headers(request.headers) 

551 

552 if not _same_origin(url, request.url): 

553 if not _is_https_redirect(request.url, url): 

554 # Strip Authorization headers when responses are redirected 

555 # away from the origin. (Except for direct HTTP to HTTPS redirects.) 

556 headers.pop("Authorization", None) 

557 

558 # Update the Host header. 

559 headers["Host"] = url.netloc.decode("ascii") 

560 

561 if method != request.method and method == "GET": 

562 # If we've switch to a 'GET' request, then strip any headers which 

563 # are only relevant to the request body. 

564 headers.pop("Content-Length", None) 

565 headers.pop("Transfer-Encoding", None) 

566 

567 # We should use the client cookie store to determine any cookie header, 

568 # rather than whatever was on the original outgoing request. 

569 headers.pop("Cookie", None) 

570 

571 return headers 

572 

573 def _redirect_stream( 

574 self, request: Request, method: str 

575 ) -> SyncByteStream | AsyncByteStream | None: 

576 """ 

577 Return the body that should be used for the redirect request. 

578 """ 

579 if method != request.method and method == "GET": 

580 return None 

581 

582 return request.stream 

583 

584 def _set_timeout(self, request: Request) -> None: 

585 if "timeout" not in request.extensions: 

586 timeout = ( 

587 self.timeout 

588 if isinstance(self.timeout, UseClientDefault) 

589 else Timeout(self.timeout) 

590 ) 

591 request.extensions = dict(**request.extensions, timeout=timeout.as_dict()) 

592 

593 

594class Client(BaseClient): 

595 """ 

596 An HTTP client, with connection pooling, HTTP/2, redirects, cookie persistence, etc. 

597 

598 It can be shared between threads. 

599 

600 Usage: 

601 

602 ```python 

603 >>> client = httpx.Client() 

604 >>> response = client.get('https://example.org') 

605 ``` 

606 

607 **Parameters:** 

608 

609 * **auth** - *(optional)* An authentication class to use when sending 

610 requests. 

611 * **params** - *(optional)* Query parameters to include in request URLs, as 

612 a string, dictionary, or sequence of two-tuples. 

613 * **headers** - *(optional)* Dictionary of HTTP headers to include when 

614 sending requests. 

615 * **cookies** - *(optional)* Dictionary of Cookie items to include when 

616 sending requests. 

617 * **verify** - *(optional)* Either `True` to use an SSL context with the 

618 default CA bundle, `False` to disable verification, or an instance of 

619 `ssl.SSLContext` to use a custom context. 

620 * **http2** - *(optional)* A boolean indicating if HTTP/2 support should be 

621 enabled. Defaults to `False`. 

622 * **proxy** - *(optional)* A proxy URL where all the traffic should be routed. 

623 * **timeout** - *(optional)* The timeout configuration to use when sending 

624 requests. 

625 * **limits** - *(optional)* The limits configuration to use. 

626 * **max_redirects** - *(optional)* The maximum number of redirect responses 

627 that should be followed. 

628 * **base_url** - *(optional)* A URL to use as the base when building 

629 request URLs. 

630 * **transport** - *(optional)* A transport class to use for sending requests 

631 over the network. 

632 * **trust_env** - *(optional)* Enables or disables usage of environment 

633 variables for configuration. 

634 * **default_encoding** - *(optional)* The default encoding to use for decoding 

635 response text, if no charset information is included in a response Content-Type 

636 header. Set to a callable for automatic character set detection. Default: "utf-8". 

637 """ 

638 

639 def __init__( 

640 self, 

641 *, 

642 auth: AuthTypes | None = None, 

643 params: QueryParamTypes | None = None, 

644 headers: HeaderTypes | None = None, 

645 cookies: CookieTypes | None = None, 

646 verify: ssl.SSLContext | str | bool = True, 

647 cert: CertTypes | None = None, 

648 trust_env: bool = True, 

649 http1: bool = True, 

650 http2: bool = False, 

651 proxy: ProxyTypes | None = None, 

652 mounts: None | (typing.Mapping[str, BaseTransport | None]) = None, 

653 timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, 

654 follow_redirects: bool = False, 

655 limits: Limits = DEFAULT_LIMITS, 

656 max_redirects: int = DEFAULT_MAX_REDIRECTS, 

657 event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, 

658 base_url: URL | str = "", 

659 transport: BaseTransport | None = None, 

660 default_encoding: str | typing.Callable[[bytes], str] = "utf-8", 

661 ) -> None: 

662 super().__init__( 

663 auth=auth, 

664 params=params, 

665 headers=headers, 

666 cookies=cookies, 

667 timeout=timeout, 

668 follow_redirects=follow_redirects, 

669 max_redirects=max_redirects, 

670 event_hooks=event_hooks, 

671 base_url=base_url, 

672 trust_env=trust_env, 

673 default_encoding=default_encoding, 

674 ) 

675 

676 if http2: 

677 try: 

678 import h2 # noqa 

679 except ImportError: # pragma: no cover 

680 raise ImportError( 

681 "Using http2=True, but the 'h2' package is not installed. " 

682 "Make sure to install httpx using `pip install httpx[http2]`." 

683 ) from None 

684 

685 allow_env_proxies = trust_env and transport is None 

686 proxy_map = self._get_proxy_map(proxy, allow_env_proxies) 

687 

688 self._transport = self._init_transport( 

689 verify=verify, 

690 cert=cert, 

691 trust_env=trust_env, 

692 http1=http1, 

693 http2=http2, 

694 limits=limits, 

695 transport=transport, 

696 ) 

697 self._mounts: dict[URLPattern, BaseTransport | None] = { 

698 URLPattern(key): None 

699 if proxy is None 

700 else self._init_proxy_transport( 

701 proxy, 

702 verify=verify, 

703 cert=cert, 

704 trust_env=trust_env, 

705 http1=http1, 

706 http2=http2, 

707 limits=limits, 

708 ) 

709 for key, proxy in proxy_map.items() 

710 } 

711 if mounts is not None: 

712 self._mounts.update( 

713 {URLPattern(key): transport for key, transport in mounts.items()} 

714 ) 

715 

716 self._mounts = dict(sorted(self._mounts.items())) 

717 

718 def _init_transport( 

719 self, 

720 verify: ssl.SSLContext | str | bool = True, 

721 cert: CertTypes | None = None, 

722 trust_env: bool = True, 

723 http1: bool = True, 

724 http2: bool = False, 

725 limits: Limits = DEFAULT_LIMITS, 

726 transport: BaseTransport | None = None, 

727 ) -> BaseTransport: 

728 if transport is not None: 

729 return transport 

730 

731 return HTTPTransport( 

732 verify=verify, 

733 cert=cert, 

734 trust_env=trust_env, 

735 http1=http1, 

736 http2=http2, 

737 limits=limits, 

738 ) 

739 

740 def _init_proxy_transport( 

741 self, 

742 proxy: Proxy, 

743 verify: ssl.SSLContext | str | bool = True, 

744 cert: CertTypes | None = None, 

745 trust_env: bool = True, 

746 http1: bool = True, 

747 http2: bool = False, 

748 limits: Limits = DEFAULT_LIMITS, 

749 ) -> BaseTransport: 

750 return HTTPTransport( 

751 verify=verify, 

752 cert=cert, 

753 trust_env=trust_env, 

754 http1=http1, 

755 http2=http2, 

756 limits=limits, 

757 proxy=proxy, 

758 ) 

759 

760 def _transport_for_url(self, url: URL) -> BaseTransport: 

761 """ 

762 Returns the transport instance that should be used for a given URL. 

763 This will either be the standard connection pool, or a proxy. 

764 """ 

765 for pattern, transport in self._mounts.items(): 

766 if pattern.matches(url): 

767 return self._transport if transport is None else transport 

768 

769 return self._transport 

770 

771 def request( 

772 self, 

773 method: str, 

774 url: URL | str, 

775 *, 

776 content: RequestContent | None = None, 

777 data: RequestData | None = None, 

778 files: RequestFiles | None = None, 

779 json: typing.Any | None = None, 

780 params: QueryParamTypes | None = None, 

781 headers: HeaderTypes | None = None, 

782 cookies: CookieTypes | None = None, 

783 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, 

784 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

785 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

786 extensions: RequestExtensions | None = None, 

787 ) -> Response: 

788 """ 

789 Build and send a request. 

790 

791 Equivalent to: 

792 

793 ```python 

794 request = client.build_request(...) 

795 response = client.send(request, ...) 

796 ``` 

797 

798 See `Client.build_request()`, `Client.send()` and 

799 [Merging of configuration][0] for how the various parameters 

800 are merged with client-level configuration. 

801 

802 [0]: /advanced/clients/#merging-of-configuration 

803 """ 

804 if cookies is not None: 

805 message = ( 

806 "Setting per-request cookies=<...> is being deprecated, because " 

807 "the expected behaviour on cookie persistence is ambiguous. Set " 

808 "cookies directly on the client instance instead." 

809 ) 

810 warnings.warn(message, DeprecationWarning, stacklevel=2) 

811 

812 request = self.build_request( 

813 method=method, 

814 url=url, 

815 content=content, 

816 data=data, 

817 files=files, 

818 json=json, 

819 params=params, 

820 headers=headers, 

821 cookies=cookies, 

822 timeout=timeout, 

823 extensions=extensions, 

824 ) 

825 return self.send(request, auth=auth, follow_redirects=follow_redirects) 

826 

827 @contextmanager 

828 def stream( 

829 self, 

830 method: str, 

831 url: URL | str, 

832 *, 

833 content: RequestContent | None = None, 

834 data: RequestData | None = None, 

835 files: RequestFiles | None = None, 

836 json: typing.Any | None = None, 

837 params: QueryParamTypes | None = None, 

838 headers: HeaderTypes | None = None, 

839 cookies: CookieTypes | None = None, 

840 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, 

841 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

842 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

843 extensions: RequestExtensions | None = None, 

844 ) -> typing.Iterator[Response]: 

845 """ 

846 Alternative to `httpx.request()` that streams the response body 

847 instead of loading it into memory at once. 

848 

849 **Parameters**: See `httpx.request`. 

850 

851 See also: [Streaming Responses][0] 

852 

853 [0]: /quickstart#streaming-responses 

854 """ 

855 request = self.build_request( 

856 method=method, 

857 url=url, 

858 content=content, 

859 data=data, 

860 files=files, 

861 json=json, 

862 params=params, 

863 headers=headers, 

864 cookies=cookies, 

865 timeout=timeout, 

866 extensions=extensions, 

867 ) 

868 response = self.send( 

869 request=request, 

870 auth=auth, 

871 follow_redirects=follow_redirects, 

872 stream=True, 

873 ) 

874 try: 

875 yield response 

876 finally: 

877 response.close() 

878 

879 def send( 

880 self, 

881 request: Request, 

882 *, 

883 stream: bool = False, 

884 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, 

885 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

886 ) -> Response: 

887 """ 

888 Send a request. 

889 

890 The request is sent as-is, unmodified. 

891 

892 Typically you'll want to build one with `Client.build_request()` 

893 so that any client-level configuration is merged into the request, 

894 but passing an explicit `httpx.Request()` is supported as well. 

895 

896 See also: [Request instances][0] 

897 

898 [0]: /advanced/clients/#request-instances 

899 """ 

900 if self._state == ClientState.CLOSED: 

901 raise RuntimeError("Cannot send a request, as the client has been closed.") 

902 

903 self._state = ClientState.OPENED 

904 follow_redirects = ( 

905 self.follow_redirects 

906 if isinstance(follow_redirects, UseClientDefault) 

907 else follow_redirects 

908 ) 

909 

910 self._set_timeout(request) 

911 

912 auth = self._build_request_auth(request, auth) 

913 

914 response = self._send_handling_auth( 

915 request, 

916 auth=auth, 

917 follow_redirects=follow_redirects, 

918 history=[], 

919 ) 

920 try: 

921 if not stream: 

922 response.read() 

923 

924 return response 

925 

926 except BaseException as exc: 

927 response.close() 

928 raise exc 

929 

930 def _send_handling_auth( 

931 self, 

932 request: Request, 

933 auth: Auth, 

934 follow_redirects: bool, 

935 history: list[Response], 

936 ) -> Response: 

937 auth_flow = auth.sync_auth_flow(request) 

938 try: 

939 request = next(auth_flow) 

940 

941 while True: 

942 response = self._send_handling_redirects( 

943 request, 

944 follow_redirects=follow_redirects, 

945 history=history, 

946 ) 

947 try: 

948 try: 

949 next_request = auth_flow.send(response) 

950 except StopIteration: 

951 return response 

952 

953 response.history = list(history) 

954 response.read() 

955 request = next_request 

956 history.append(response) 

957 

958 except BaseException as exc: 

959 response.close() 

960 raise exc 

961 finally: 

962 auth_flow.close() 

963 

964 def _send_handling_redirects( 

965 self, 

966 request: Request, 

967 follow_redirects: bool, 

968 history: list[Response], 

969 ) -> Response: 

970 while True: 

971 if len(history) > self.max_redirects: 

972 raise TooManyRedirects( 

973 "Exceeded maximum allowed redirects.", request=request 

974 ) 

975 

976 for hook in self._event_hooks["request"]: 

977 hook(request) 

978 

979 response = self._send_single_request(request) 

980 try: 

981 for hook in self._event_hooks["response"]: 

982 hook(response) 

983 response.history = list(history) 

984 

985 if not response.has_redirect_location: 

986 return response 

987 

988 request = self._build_redirect_request(request, response) 

989 history = history + [response] 

990 

991 if follow_redirects: 

992 response.read() 

993 else: 

994 response.next_request = request 

995 return response 

996 

997 except BaseException as exc: 

998 response.close() 

999 raise exc 

1000 

1001 def _send_single_request(self, request: Request) -> Response: 

1002 """ 

1003 Sends a single request, without handling any redirections. 

1004 """ 

1005 transport = self._transport_for_url(request.url) 

1006 start = time.perf_counter() 

1007 

1008 if not isinstance(request.stream, SyncByteStream): 

1009 raise RuntimeError( 

1010 "Attempted to send an async request with a sync Client instance." 

1011 ) 

1012 

1013 with request_context(request=request): 

1014 response = transport.handle_request(request) 

1015 

1016 assert isinstance(response.stream, SyncByteStream) 

1017 

1018 response.request = request 

1019 response.stream = BoundSyncStream( 

1020 response.stream, response=response, start=start 

1021 ) 

1022 self.cookies.extract_cookies(response) 

1023 response.default_encoding = self._default_encoding 

1024 

1025 logger.info( 

1026 'HTTP Request: %s %s "%s %d %s"', 

1027 request.method, 

1028 request.url, 

1029 response.http_version, 

1030 response.status_code, 

1031 response.reason_phrase, 

1032 ) 

1033 

1034 return response 

1035 

1036 def get( 

1037 self, 

1038 url: URL | str, 

1039 *, 

1040 params: QueryParamTypes | None = None, 

1041 headers: HeaderTypes | None = None, 

1042 cookies: CookieTypes | None = None, 

1043 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, 

1044 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1045 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1046 extensions: RequestExtensions | None = None, 

1047 ) -> Response: 

1048 """ 

1049 Send a `GET` request. 

1050 

1051 **Parameters**: See `httpx.request`. 

1052 """ 

1053 return self.request( 

1054 "GET", 

1055 url, 

1056 params=params, 

1057 headers=headers, 

1058 cookies=cookies, 

1059 auth=auth, 

1060 follow_redirects=follow_redirects, 

1061 timeout=timeout, 

1062 extensions=extensions, 

1063 ) 

1064 

1065 def options( 

1066 self, 

1067 url: URL | str, 

1068 *, 

1069 params: QueryParamTypes | None = None, 

1070 headers: HeaderTypes | None = None, 

1071 cookies: CookieTypes | None = None, 

1072 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1073 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1074 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1075 extensions: RequestExtensions | None = None, 

1076 ) -> Response: 

1077 """ 

1078 Send an `OPTIONS` request. 

1079 

1080 **Parameters**: See `httpx.request`. 

1081 """ 

1082 return self.request( 

1083 "OPTIONS", 

1084 url, 

1085 params=params, 

1086 headers=headers, 

1087 cookies=cookies, 

1088 auth=auth, 

1089 follow_redirects=follow_redirects, 

1090 timeout=timeout, 

1091 extensions=extensions, 

1092 ) 

1093 

1094 def head( 

1095 self, 

1096 url: URL | str, 

1097 *, 

1098 params: QueryParamTypes | None = None, 

1099 headers: HeaderTypes | None = None, 

1100 cookies: CookieTypes | None = None, 

1101 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1102 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1103 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1104 extensions: RequestExtensions | None = None, 

1105 ) -> Response: 

1106 """ 

1107 Send a `HEAD` request. 

1108 

1109 **Parameters**: See `httpx.request`. 

1110 """ 

1111 return self.request( 

1112 "HEAD", 

1113 url, 

1114 params=params, 

1115 headers=headers, 

1116 cookies=cookies, 

1117 auth=auth, 

1118 follow_redirects=follow_redirects, 

1119 timeout=timeout, 

1120 extensions=extensions, 

1121 ) 

1122 

1123 def post( 

1124 self, 

1125 url: URL | str, 

1126 *, 

1127 content: RequestContent | None = None, 

1128 data: RequestData | None = None, 

1129 files: RequestFiles | None = None, 

1130 json: typing.Any | None = None, 

1131 params: QueryParamTypes | None = None, 

1132 headers: HeaderTypes | None = None, 

1133 cookies: CookieTypes | None = None, 

1134 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1135 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1136 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1137 extensions: RequestExtensions | None = None, 

1138 ) -> Response: 

1139 """ 

1140 Send a `POST` request. 

1141 

1142 **Parameters**: See `httpx.request`. 

1143 """ 

1144 return self.request( 

1145 "POST", 

1146 url, 

1147 content=content, 

1148 data=data, 

1149 files=files, 

1150 json=json, 

1151 params=params, 

1152 headers=headers, 

1153 cookies=cookies, 

1154 auth=auth, 

1155 follow_redirects=follow_redirects, 

1156 timeout=timeout, 

1157 extensions=extensions, 

1158 ) 

1159 

1160 def put( 

1161 self, 

1162 url: URL | str, 

1163 *, 

1164 content: RequestContent | None = None, 

1165 data: RequestData | None = None, 

1166 files: RequestFiles | None = None, 

1167 json: typing.Any | None = None, 

1168 params: QueryParamTypes | None = None, 

1169 headers: HeaderTypes | None = None, 

1170 cookies: CookieTypes | None = None, 

1171 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1172 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1173 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1174 extensions: RequestExtensions | None = None, 

1175 ) -> Response: 

1176 """ 

1177 Send a `PUT` request. 

1178 

1179 **Parameters**: See `httpx.request`. 

1180 """ 

1181 return self.request( 

1182 "PUT", 

1183 url, 

1184 content=content, 

1185 data=data, 

1186 files=files, 

1187 json=json, 

1188 params=params, 

1189 headers=headers, 

1190 cookies=cookies, 

1191 auth=auth, 

1192 follow_redirects=follow_redirects, 

1193 timeout=timeout, 

1194 extensions=extensions, 

1195 ) 

1196 

1197 def patch( 

1198 self, 

1199 url: URL | str, 

1200 *, 

1201 content: RequestContent | None = None, 

1202 data: RequestData | None = None, 

1203 files: RequestFiles | None = None, 

1204 json: typing.Any | None = None, 

1205 params: QueryParamTypes | None = None, 

1206 headers: HeaderTypes | None = None, 

1207 cookies: CookieTypes | None = None, 

1208 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1209 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1210 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1211 extensions: RequestExtensions | None = None, 

1212 ) -> Response: 

1213 """ 

1214 Send a `PATCH` request. 

1215 

1216 **Parameters**: See `httpx.request`. 

1217 """ 

1218 return self.request( 

1219 "PATCH", 

1220 url, 

1221 content=content, 

1222 data=data, 

1223 files=files, 

1224 json=json, 

1225 params=params, 

1226 headers=headers, 

1227 cookies=cookies, 

1228 auth=auth, 

1229 follow_redirects=follow_redirects, 

1230 timeout=timeout, 

1231 extensions=extensions, 

1232 ) 

1233 

1234 def delete( 

1235 self, 

1236 url: URL | str, 

1237 *, 

1238 params: QueryParamTypes | None = None, 

1239 headers: HeaderTypes | None = None, 

1240 cookies: CookieTypes | None = None, 

1241 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1242 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1243 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1244 extensions: RequestExtensions | None = None, 

1245 ) -> Response: 

1246 """ 

1247 Send a `DELETE` request. 

1248 

1249 **Parameters**: See `httpx.request`. 

1250 """ 

1251 return self.request( 

1252 "DELETE", 

1253 url, 

1254 params=params, 

1255 headers=headers, 

1256 cookies=cookies, 

1257 auth=auth, 

1258 follow_redirects=follow_redirects, 

1259 timeout=timeout, 

1260 extensions=extensions, 

1261 ) 

1262 

1263 def close(self) -> None: 

1264 """ 

1265 Close transport and proxies. 

1266 """ 

1267 if self._state != ClientState.CLOSED: 

1268 self._state = ClientState.CLOSED 

1269 

1270 self._transport.close() 

1271 for transport in self._mounts.values(): 

1272 if transport is not None: 

1273 transport.close() 

1274 

1275 def __enter__(self: T) -> T: 

1276 if self._state != ClientState.UNOPENED: 

1277 msg = { 

1278 ClientState.OPENED: "Cannot open a client instance more than once.", 

1279 ClientState.CLOSED: ( 

1280 "Cannot reopen a client instance, once it has been closed." 

1281 ), 

1282 }[self._state] 

1283 raise RuntimeError(msg) 

1284 

1285 self._state = ClientState.OPENED 

1286 

1287 self._transport.__enter__() 

1288 for transport in self._mounts.values(): 

1289 if transport is not None: 

1290 transport.__enter__() 

1291 return self 

1292 

1293 def __exit__( 

1294 self, 

1295 exc_type: type[BaseException] | None = None, 

1296 exc_value: BaseException | None = None, 

1297 traceback: TracebackType | None = None, 

1298 ) -> None: 

1299 self._state = ClientState.CLOSED 

1300 

1301 self._transport.__exit__(exc_type, exc_value, traceback) 

1302 for transport in self._mounts.values(): 

1303 if transport is not None: 

1304 transport.__exit__(exc_type, exc_value, traceback) 

1305 

1306 

1307class AsyncClient(BaseClient): 

1308 """ 

1309 An asynchronous HTTP client, with connection pooling, HTTP/2, redirects, 

1310 cookie persistence, etc. 

1311 

1312 It can be shared between tasks. 

1313 

1314 Usage: 

1315 

1316 ```python 

1317 >>> async with httpx.AsyncClient() as client: 

1318 >>> response = await client.get('https://example.org') 

1319 ``` 

1320 

1321 **Parameters:** 

1322 

1323 * **auth** - *(optional)* An authentication class to use when sending 

1324 requests. 

1325 * **params** - *(optional)* Query parameters to include in request URLs, as 

1326 a string, dictionary, or sequence of two-tuples. 

1327 * **headers** - *(optional)* Dictionary of HTTP headers to include when 

1328 sending requests. 

1329 * **cookies** - *(optional)* Dictionary of Cookie items to include when 

1330 sending requests. 

1331 * **verify** - *(optional)* Either `True` to use an SSL context with the 

1332 default CA bundle, `False` to disable verification, or an instance of 

1333 `ssl.SSLContext` to use a custom context. 

1334 * **http2** - *(optional)* A boolean indicating if HTTP/2 support should be 

1335 enabled. Defaults to `False`. 

1336 * **proxy** - *(optional)* A proxy URL where all the traffic should be routed. 

1337 * **timeout** - *(optional)* The timeout configuration to use when sending 

1338 requests. 

1339 * **limits** - *(optional)* The limits configuration to use. 

1340 * **max_redirects** - *(optional)* The maximum number of redirect responses 

1341 that should be followed. 

1342 * **base_url** - *(optional)* A URL to use as the base when building 

1343 request URLs. 

1344 * **transport** - *(optional)* A transport class to use for sending requests 

1345 over the network. 

1346 * **trust_env** - *(optional)* Enables or disables usage of environment 

1347 variables for configuration. 

1348 * **default_encoding** - *(optional)* The default encoding to use for decoding 

1349 response text, if no charset information is included in a response Content-Type 

1350 header. Set to a callable for automatic character set detection. Default: "utf-8". 

1351 """ 

1352 

1353 def __init__( 

1354 self, 

1355 *, 

1356 auth: AuthTypes | None = None, 

1357 params: QueryParamTypes | None = None, 

1358 headers: HeaderTypes | None = None, 

1359 cookies: CookieTypes | None = None, 

1360 verify: ssl.SSLContext | str | bool = True, 

1361 cert: CertTypes | None = None, 

1362 http1: bool = True, 

1363 http2: bool = False, 

1364 proxy: ProxyTypes | None = None, 

1365 mounts: None | (typing.Mapping[str, AsyncBaseTransport | None]) = None, 

1366 timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, 

1367 follow_redirects: bool = False, 

1368 limits: Limits = DEFAULT_LIMITS, 

1369 max_redirects: int = DEFAULT_MAX_REDIRECTS, 

1370 event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, 

1371 base_url: URL | str = "", 

1372 transport: AsyncBaseTransport | None = None, 

1373 trust_env: bool = True, 

1374 default_encoding: str | typing.Callable[[bytes], str] = "utf-8", 

1375 ) -> None: 

1376 super().__init__( 

1377 auth=auth, 

1378 params=params, 

1379 headers=headers, 

1380 cookies=cookies, 

1381 timeout=timeout, 

1382 follow_redirects=follow_redirects, 

1383 max_redirects=max_redirects, 

1384 event_hooks=event_hooks, 

1385 base_url=base_url, 

1386 trust_env=trust_env, 

1387 default_encoding=default_encoding, 

1388 ) 

1389 

1390 if http2: 

1391 try: 

1392 import h2 # noqa 

1393 except ImportError: # pragma: no cover 

1394 raise ImportError( 

1395 "Using http2=True, but the 'h2' package is not installed. " 

1396 "Make sure to install httpx using `pip install httpx[http2]`." 

1397 ) from None 

1398 

1399 allow_env_proxies = trust_env and transport is None 

1400 proxy_map = self._get_proxy_map(proxy, allow_env_proxies) 

1401 

1402 self._transport = self._init_transport( 

1403 verify=verify, 

1404 cert=cert, 

1405 trust_env=trust_env, 

1406 http1=http1, 

1407 http2=http2, 

1408 limits=limits, 

1409 transport=transport, 

1410 ) 

1411 

1412 self._mounts: dict[URLPattern, AsyncBaseTransport | None] = { 

1413 URLPattern(key): None 

1414 if proxy is None 

1415 else self._init_proxy_transport( 

1416 proxy, 

1417 verify=verify, 

1418 cert=cert, 

1419 trust_env=trust_env, 

1420 http1=http1, 

1421 http2=http2, 

1422 limits=limits, 

1423 ) 

1424 for key, proxy in proxy_map.items() 

1425 } 

1426 if mounts is not None: 

1427 self._mounts.update( 

1428 {URLPattern(key): transport for key, transport in mounts.items()} 

1429 ) 

1430 self._mounts = dict(sorted(self._mounts.items())) 

1431 

1432 def _init_transport( 

1433 self, 

1434 verify: ssl.SSLContext | str | bool = True, 

1435 cert: CertTypes | None = None, 

1436 trust_env: bool = True, 

1437 http1: bool = True, 

1438 http2: bool = False, 

1439 limits: Limits = DEFAULT_LIMITS, 

1440 transport: AsyncBaseTransport | None = None, 

1441 ) -> AsyncBaseTransport: 

1442 if transport is not None: 

1443 return transport 

1444 

1445 return AsyncHTTPTransport( 

1446 verify=verify, 

1447 cert=cert, 

1448 trust_env=trust_env, 

1449 http1=http1, 

1450 http2=http2, 

1451 limits=limits, 

1452 ) 

1453 

1454 def _init_proxy_transport( 

1455 self, 

1456 proxy: Proxy, 

1457 verify: ssl.SSLContext | str | bool = True, 

1458 cert: CertTypes | None = None, 

1459 trust_env: bool = True, 

1460 http1: bool = True, 

1461 http2: bool = False, 

1462 limits: Limits = DEFAULT_LIMITS, 

1463 ) -> AsyncBaseTransport: 

1464 return AsyncHTTPTransport( 

1465 verify=verify, 

1466 cert=cert, 

1467 trust_env=trust_env, 

1468 http1=http1, 

1469 http2=http2, 

1470 limits=limits, 

1471 proxy=proxy, 

1472 ) 

1473 

1474 def _transport_for_url(self, url: URL) -> AsyncBaseTransport: 

1475 """ 

1476 Returns the transport instance that should be used for a given URL. 

1477 This will either be the standard connection pool, or a proxy. 

1478 """ 

1479 for pattern, transport in self._mounts.items(): 

1480 if pattern.matches(url): 

1481 return self._transport if transport is None else transport 

1482 

1483 return self._transport 

1484 

1485 async def request( 

1486 self, 

1487 method: str, 

1488 url: URL | str, 

1489 *, 

1490 content: RequestContent | None = None, 

1491 data: RequestData | None = None, 

1492 files: RequestFiles | None = None, 

1493 json: typing.Any | None = None, 

1494 params: QueryParamTypes | None = None, 

1495 headers: HeaderTypes | None = None, 

1496 cookies: CookieTypes | None = None, 

1497 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, 

1498 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1499 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1500 extensions: RequestExtensions | None = None, 

1501 ) -> Response: 

1502 """ 

1503 Build and send a request. 

1504 

1505 Equivalent to: 

1506 

1507 ```python 

1508 request = client.build_request(...) 

1509 response = await client.send(request, ...) 

1510 ``` 

1511 

1512 See `AsyncClient.build_request()`, `AsyncClient.send()` 

1513 and [Merging of configuration][0] for how the various parameters 

1514 are merged with client-level configuration. 

1515 

1516 [0]: /advanced/clients/#merging-of-configuration 

1517 """ 

1518 

1519 if cookies is not None: # pragma: no cover 

1520 message = ( 

1521 "Setting per-request cookies=<...> is being deprecated, because " 

1522 "the expected behaviour on cookie persistence is ambiguous. Set " 

1523 "cookies directly on the client instance instead." 

1524 ) 

1525 warnings.warn(message, DeprecationWarning, stacklevel=2) 

1526 

1527 request = self.build_request( 

1528 method=method, 

1529 url=url, 

1530 content=content, 

1531 data=data, 

1532 files=files, 

1533 json=json, 

1534 params=params, 

1535 headers=headers, 

1536 cookies=cookies, 

1537 timeout=timeout, 

1538 extensions=extensions, 

1539 ) 

1540 return await self.send(request, auth=auth, follow_redirects=follow_redirects) 

1541 

1542 @asynccontextmanager 

1543 async def stream( 

1544 self, 

1545 method: str, 

1546 url: URL | str, 

1547 *, 

1548 content: RequestContent | None = None, 

1549 data: RequestData | None = None, 

1550 files: RequestFiles | None = None, 

1551 json: typing.Any | None = None, 

1552 params: QueryParamTypes | None = None, 

1553 headers: HeaderTypes | None = None, 

1554 cookies: CookieTypes | None = None, 

1555 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, 

1556 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1557 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1558 extensions: RequestExtensions | None = None, 

1559 ) -> typing.AsyncIterator[Response]: 

1560 """ 

1561 Alternative to `httpx.request()` that streams the response body 

1562 instead of loading it into memory at once. 

1563 

1564 **Parameters**: See `httpx.request`. 

1565 

1566 See also: [Streaming Responses][0] 

1567 

1568 [0]: /quickstart#streaming-responses 

1569 """ 

1570 request = self.build_request( 

1571 method=method, 

1572 url=url, 

1573 content=content, 

1574 data=data, 

1575 files=files, 

1576 json=json, 

1577 params=params, 

1578 headers=headers, 

1579 cookies=cookies, 

1580 timeout=timeout, 

1581 extensions=extensions, 

1582 ) 

1583 response = await self.send( 

1584 request=request, 

1585 auth=auth, 

1586 follow_redirects=follow_redirects, 

1587 stream=True, 

1588 ) 

1589 try: 

1590 yield response 

1591 finally: 

1592 await response.aclose() 

1593 

1594 async def send( 

1595 self, 

1596 request: Request, 

1597 *, 

1598 stream: bool = False, 

1599 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, 

1600 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1601 ) -> Response: 

1602 """ 

1603 Send a request. 

1604 

1605 The request is sent as-is, unmodified. 

1606 

1607 Typically you'll want to build one with `AsyncClient.build_request()` 

1608 so that any client-level configuration is merged into the request, 

1609 but passing an explicit `httpx.Request()` is supported as well. 

1610 

1611 See also: [Request instances][0] 

1612 

1613 [0]: /advanced/clients/#request-instances 

1614 """ 

1615 if self._state == ClientState.CLOSED: 

1616 raise RuntimeError("Cannot send a request, as the client has been closed.") 

1617 

1618 self._state = ClientState.OPENED 

1619 follow_redirects = ( 

1620 self.follow_redirects 

1621 if isinstance(follow_redirects, UseClientDefault) 

1622 else follow_redirects 

1623 ) 

1624 

1625 self._set_timeout(request) 

1626 

1627 auth = self._build_request_auth(request, auth) 

1628 

1629 response = await self._send_handling_auth( 

1630 request, 

1631 auth=auth, 

1632 follow_redirects=follow_redirects, 

1633 history=[], 

1634 ) 

1635 try: 

1636 if not stream: 

1637 await response.aread() 

1638 

1639 return response 

1640 

1641 except BaseException as exc: 

1642 await response.aclose() 

1643 raise exc 

1644 

1645 async def _send_handling_auth( 

1646 self, 

1647 request: Request, 

1648 auth: Auth, 

1649 follow_redirects: bool, 

1650 history: list[Response], 

1651 ) -> Response: 

1652 auth_flow = auth.async_auth_flow(request) 

1653 try: 

1654 request = await auth_flow.__anext__() 

1655 

1656 while True: 

1657 response = await self._send_handling_redirects( 

1658 request, 

1659 follow_redirects=follow_redirects, 

1660 history=history, 

1661 ) 

1662 try: 

1663 try: 

1664 next_request = await auth_flow.asend(response) 

1665 except StopAsyncIteration: 

1666 return response 

1667 

1668 response.history = list(history) 

1669 await response.aread() 

1670 request = next_request 

1671 history.append(response) 

1672 

1673 except BaseException as exc: 

1674 await response.aclose() 

1675 raise exc 

1676 finally: 

1677 await auth_flow.aclose() 

1678 

1679 async def _send_handling_redirects( 

1680 self, 

1681 request: Request, 

1682 follow_redirects: bool, 

1683 history: list[Response], 

1684 ) -> Response: 

1685 while True: 

1686 if len(history) > self.max_redirects: 

1687 raise TooManyRedirects( 

1688 "Exceeded maximum allowed redirects.", request=request 

1689 ) 

1690 

1691 for hook in self._event_hooks["request"]: 

1692 await hook(request) 

1693 

1694 response = await self._send_single_request(request) 

1695 try: 

1696 for hook in self._event_hooks["response"]: 

1697 await hook(response) 

1698 

1699 response.history = list(history) 

1700 

1701 if not response.has_redirect_location: 

1702 return response 

1703 

1704 request = self._build_redirect_request(request, response) 

1705 history = history + [response] 

1706 

1707 if follow_redirects: 

1708 await response.aread() 

1709 else: 

1710 response.next_request = request 

1711 return response 

1712 

1713 except BaseException as exc: 

1714 await response.aclose() 

1715 raise exc 

1716 

1717 async def _send_single_request(self, request: Request) -> Response: 

1718 """ 

1719 Sends a single request, without handling any redirections. 

1720 """ 

1721 transport = self._transport_for_url(request.url) 

1722 start = time.perf_counter() 

1723 

1724 if not isinstance(request.stream, AsyncByteStream): 

1725 raise RuntimeError( 

1726 "Attempted to send an sync request with an AsyncClient instance." 

1727 ) 

1728 

1729 with request_context(request=request): 

1730 response = await transport.handle_async_request(request) 

1731 

1732 assert isinstance(response.stream, AsyncByteStream) 

1733 response.request = request 

1734 response.stream = BoundAsyncStream( 

1735 response.stream, response=response, start=start 

1736 ) 

1737 self.cookies.extract_cookies(response) 

1738 response.default_encoding = self._default_encoding 

1739 

1740 logger.info( 

1741 'HTTP Request: %s %s "%s %d %s"', 

1742 request.method, 

1743 request.url, 

1744 response.http_version, 

1745 response.status_code, 

1746 response.reason_phrase, 

1747 ) 

1748 

1749 return response 

1750 

1751 async def get( 

1752 self, 

1753 url: URL | str, 

1754 *, 

1755 params: QueryParamTypes | None = None, 

1756 headers: HeaderTypes | None = None, 

1757 cookies: CookieTypes | None = None, 

1758 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, 

1759 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1760 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1761 extensions: RequestExtensions | None = None, 

1762 ) -> Response: 

1763 """ 

1764 Send a `GET` request. 

1765 

1766 **Parameters**: See `httpx.request`. 

1767 """ 

1768 return await self.request( 

1769 "GET", 

1770 url, 

1771 params=params, 

1772 headers=headers, 

1773 cookies=cookies, 

1774 auth=auth, 

1775 follow_redirects=follow_redirects, 

1776 timeout=timeout, 

1777 extensions=extensions, 

1778 ) 

1779 

1780 async def options( 

1781 self, 

1782 url: URL | str, 

1783 *, 

1784 params: QueryParamTypes | None = None, 

1785 headers: HeaderTypes | None = None, 

1786 cookies: CookieTypes | None = None, 

1787 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1788 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1789 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1790 extensions: RequestExtensions | None = None, 

1791 ) -> Response: 

1792 """ 

1793 Send an `OPTIONS` request. 

1794 

1795 **Parameters**: See `httpx.request`. 

1796 """ 

1797 return await self.request( 

1798 "OPTIONS", 

1799 url, 

1800 params=params, 

1801 headers=headers, 

1802 cookies=cookies, 

1803 auth=auth, 

1804 follow_redirects=follow_redirects, 

1805 timeout=timeout, 

1806 extensions=extensions, 

1807 ) 

1808 

1809 async def head( 

1810 self, 

1811 url: URL | str, 

1812 *, 

1813 params: QueryParamTypes | None = None, 

1814 headers: HeaderTypes | None = None, 

1815 cookies: CookieTypes | None = None, 

1816 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1817 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1818 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1819 extensions: RequestExtensions | None = None, 

1820 ) -> Response: 

1821 """ 

1822 Send a `HEAD` request. 

1823 

1824 **Parameters**: See `httpx.request`. 

1825 """ 

1826 return await self.request( 

1827 "HEAD", 

1828 url, 

1829 params=params, 

1830 headers=headers, 

1831 cookies=cookies, 

1832 auth=auth, 

1833 follow_redirects=follow_redirects, 

1834 timeout=timeout, 

1835 extensions=extensions, 

1836 ) 

1837 

1838 async def post( 

1839 self, 

1840 url: URL | str, 

1841 *, 

1842 content: RequestContent | None = None, 

1843 data: RequestData | None = None, 

1844 files: RequestFiles | None = None, 

1845 json: typing.Any | None = None, 

1846 params: QueryParamTypes | None = None, 

1847 headers: HeaderTypes | None = None, 

1848 cookies: CookieTypes | None = None, 

1849 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1850 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1851 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1852 extensions: RequestExtensions | None = None, 

1853 ) -> Response: 

1854 """ 

1855 Send a `POST` request. 

1856 

1857 **Parameters**: See `httpx.request`. 

1858 """ 

1859 return await self.request( 

1860 "POST", 

1861 url, 

1862 content=content, 

1863 data=data, 

1864 files=files, 

1865 json=json, 

1866 params=params, 

1867 headers=headers, 

1868 cookies=cookies, 

1869 auth=auth, 

1870 follow_redirects=follow_redirects, 

1871 timeout=timeout, 

1872 extensions=extensions, 

1873 ) 

1874 

1875 async def put( 

1876 self, 

1877 url: URL | str, 

1878 *, 

1879 content: RequestContent | None = None, 

1880 data: RequestData | None = None, 

1881 files: RequestFiles | None = None, 

1882 json: typing.Any | None = None, 

1883 params: QueryParamTypes | None = None, 

1884 headers: HeaderTypes | None = None, 

1885 cookies: CookieTypes | None = None, 

1886 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1887 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1888 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1889 extensions: RequestExtensions | None = None, 

1890 ) -> Response: 

1891 """ 

1892 Send a `PUT` request. 

1893 

1894 **Parameters**: See `httpx.request`. 

1895 """ 

1896 return await self.request( 

1897 "PUT", 

1898 url, 

1899 content=content, 

1900 data=data, 

1901 files=files, 

1902 json=json, 

1903 params=params, 

1904 headers=headers, 

1905 cookies=cookies, 

1906 auth=auth, 

1907 follow_redirects=follow_redirects, 

1908 timeout=timeout, 

1909 extensions=extensions, 

1910 ) 

1911 

1912 async def patch( 

1913 self, 

1914 url: URL | str, 

1915 *, 

1916 content: RequestContent | None = None, 

1917 data: RequestData | None = None, 

1918 files: RequestFiles | None = None, 

1919 json: typing.Any | None = None, 

1920 params: QueryParamTypes | None = None, 

1921 headers: HeaderTypes | None = None, 

1922 cookies: CookieTypes | None = None, 

1923 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1924 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1925 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1926 extensions: RequestExtensions | None = None, 

1927 ) -> Response: 

1928 """ 

1929 Send a `PATCH` request. 

1930 

1931 **Parameters**: See `httpx.request`. 

1932 """ 

1933 return await self.request( 

1934 "PATCH", 

1935 url, 

1936 content=content, 

1937 data=data, 

1938 files=files, 

1939 json=json, 

1940 params=params, 

1941 headers=headers, 

1942 cookies=cookies, 

1943 auth=auth, 

1944 follow_redirects=follow_redirects, 

1945 timeout=timeout, 

1946 extensions=extensions, 

1947 ) 

1948 

1949 async def delete( 

1950 self, 

1951 url: URL | str, 

1952 *, 

1953 params: QueryParamTypes | None = None, 

1954 headers: HeaderTypes | None = None, 

1955 cookies: CookieTypes | None = None, 

1956 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1957 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1958 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1959 extensions: RequestExtensions | None = None, 

1960 ) -> Response: 

1961 """ 

1962 Send a `DELETE` request. 

1963 

1964 **Parameters**: See `httpx.request`. 

1965 """ 

1966 return await self.request( 

1967 "DELETE", 

1968 url, 

1969 params=params, 

1970 headers=headers, 

1971 cookies=cookies, 

1972 auth=auth, 

1973 follow_redirects=follow_redirects, 

1974 timeout=timeout, 

1975 extensions=extensions, 

1976 ) 

1977 

1978 async def aclose(self) -> None: 

1979 """ 

1980 Close transport and proxies. 

1981 """ 

1982 if self._state != ClientState.CLOSED: 

1983 self._state = ClientState.CLOSED 

1984 

1985 await self._transport.aclose() 

1986 for proxy in self._mounts.values(): 

1987 if proxy is not None: 

1988 await proxy.aclose() 

1989 

1990 async def __aenter__(self: U) -> U: 

1991 if self._state != ClientState.UNOPENED: 

1992 msg = { 

1993 ClientState.OPENED: "Cannot open a client instance more than once.", 

1994 ClientState.CLOSED: ( 

1995 "Cannot reopen a client instance, once it has been closed." 

1996 ), 

1997 }[self._state] 

1998 raise RuntimeError(msg) 

1999 

2000 self._state = ClientState.OPENED 

2001 

2002 await self._transport.__aenter__() 

2003 for proxy in self._mounts.values(): 

2004 if proxy is not None: 

2005 await proxy.__aenter__() 

2006 return self 

2007 

2008 async def __aexit__( 

2009 self, 

2010 exc_type: type[BaseException] | None = None, 

2011 exc_value: BaseException | None = None, 

2012 traceback: TracebackType | None = None, 

2013 ) -> None: 

2014 self._state = ClientState.CLOSED 

2015 

2016 await self._transport.__aexit__(exc_type, exc_value, traceback) 

2017 for proxy in self._mounts.values(): 

2018 if proxy is not None: 

2019 await proxy.__aexit__(exc_type, exc_value, traceback)