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

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

541 statements  

1from __future__ import annotations 

2 

3import datetime 

4import enum 

5import logging 

6import typing 

7import warnings 

8from contextlib import asynccontextmanager, contextmanager 

9from types import TracebackType 

10 

11from .__version__ import __version__ 

12from ._auth import Auth, BasicAuth, FunctionAuth 

13from ._config import ( 

14 DEFAULT_LIMITS, 

15 DEFAULT_MAX_REDIRECTS, 

16 DEFAULT_TIMEOUT_CONFIG, 

17 Limits, 

18 Proxy, 

19 Timeout, 

20) 

21from ._decoders import SUPPORTED_DECODERS 

22from ._exceptions import ( 

23 InvalidURL, 

24 RemoteProtocolError, 

25 TooManyRedirects, 

26 request_context, 

27) 

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

29from ._status_codes import codes 

30from ._transports.asgi import ASGITransport 

31from ._transports.base import AsyncBaseTransport, BaseTransport 

32from ._transports.default import AsyncHTTPTransport, HTTPTransport 

33from ._transports.wsgi import WSGITransport 

34from ._types import ( 

35 AsyncByteStream, 

36 AuthTypes, 

37 CertTypes, 

38 CookieTypes, 

39 HeaderTypes, 

40 ProxiesTypes, 

41 ProxyTypes, 

42 QueryParamTypes, 

43 RequestContent, 

44 RequestData, 

45 RequestExtensions, 

46 RequestFiles, 

47 SyncByteStream, 

48 TimeoutTypes, 

49 VerifyTypes, 

50) 

51from ._urls import URL, QueryParams 

52from ._utils import ( 

53 Timer, 

54 URLPattern, 

55 get_environment_proxies, 

56 is_https_redirect, 

57 same_origin, 

58) 

59 

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

61 

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

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

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

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

66 

67 

68class UseClientDefault: 

69 """ 

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

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

72 to using `None`. 

73 

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

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

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

77 

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

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

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

81 ensure no timeout is used. 

82 

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

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

85 """ 

86 

87 

88USE_CLIENT_DEFAULT = UseClientDefault() 

89 

90 

91logger = logging.getLogger("httpx") 

92 

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

94ACCEPT_ENCODING = ", ".join( 

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

96) 

97 

98 

99class ClientState(enum.Enum): 

100 # UNOPENED: 

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

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

103 UNOPENED = 1 

104 # OPENED: 

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

106 OPENED = 2 

107 # CLOSED: 

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

109 # been called explicitly. 

110 CLOSED = 3 

111 

112 

113class BoundSyncStream(SyncByteStream): 

114 """ 

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

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

117 """ 

118 

119 def __init__( 

120 self, stream: SyncByteStream, response: Response, timer: Timer 

121 ) -> None: 

122 self._stream = stream 

123 self._response = response 

124 self._timer = timer 

125 

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

127 for chunk in self._stream: 

128 yield chunk 

129 

130 def close(self) -> None: 

131 seconds = self._timer.sync_elapsed() 

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

133 self._stream.close() 

134 

135 

136class BoundAsyncStream(AsyncByteStream): 

137 """ 

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

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

140 """ 

141 

142 def __init__( 

143 self, stream: AsyncByteStream, response: Response, timer: Timer 

144 ) -> None: 

145 self._stream = stream 

146 self._response = response 

147 self._timer = timer 

148 

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

150 async for chunk in self._stream: 

151 yield chunk 

152 

153 async def aclose(self) -> None: 

154 seconds = await self._timer.async_elapsed() 

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

156 await self._stream.aclose() 

157 

158 

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

160 

161 

162class BaseClient: 

163 def __init__( 

164 self, 

165 *, 

166 auth: AuthTypes | None = None, 

167 params: QueryParamTypes | None = None, 

168 headers: HeaderTypes | None = None, 

169 cookies: CookieTypes | None = None, 

170 timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, 

171 follow_redirects: bool = False, 

172 max_redirects: int = DEFAULT_MAX_REDIRECTS, 

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

174 base_url: URL | str = "", 

175 trust_env: bool = True, 

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

177 ) -> None: 

178 event_hooks = {} if event_hooks is None else event_hooks 

179 

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

181 

182 self._auth = self._build_auth(auth) 

183 self._params = QueryParams(params) 

184 self.headers = Headers(headers) 

185 self._cookies = Cookies(cookies) 

186 self._timeout = Timeout(timeout) 

187 self.follow_redirects = follow_redirects 

188 self.max_redirects = max_redirects 

189 self._event_hooks = { 

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

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

192 } 

193 self._trust_env = trust_env 

194 self._default_encoding = default_encoding 

195 self._state = ClientState.UNOPENED 

196 

197 @property 

198 def is_closed(self) -> bool: 

199 """ 

200 Check if the client being closed 

201 """ 

202 return self._state == ClientState.CLOSED 

203 

204 @property 

205 def trust_env(self) -> bool: 

206 return self._trust_env 

207 

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

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

210 return url 

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

212 

213 def _get_proxy_map( 

214 self, proxies: ProxiesTypes | None, allow_env_proxies: bool 

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

216 if proxies is None: 

217 if allow_env_proxies: 

218 return { 

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

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

221 } 

222 return {} 

223 if isinstance(proxies, dict): 

224 new_proxies = {} 

225 for key, value in proxies.items(): 

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

227 new_proxies[str(key)] = proxy 

228 return new_proxies 

229 else: 

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

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

232 

233 @property 

234 def timeout(self) -> Timeout: 

235 return self._timeout 

236 

237 @timeout.setter 

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

239 self._timeout = Timeout(timeout) 

240 

241 @property 

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

243 return self._event_hooks 

244 

245 @event_hooks.setter 

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

247 self._event_hooks = { 

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

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

250 } 

251 

252 @property 

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

254 """ 

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

256 

257 See also [Authentication][0]. 

258 

259 [0]: /quickstart/#authentication 

260 """ 

261 return self._auth 

262 

263 @auth.setter 

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

265 self._auth = self._build_auth(auth) 

266 

267 @property 

268 def base_url(self) -> URL: 

269 """ 

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

271 """ 

272 return self._base_url 

273 

274 @base_url.setter 

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

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

277 

278 @property 

279 def headers(self) -> Headers: 

280 """ 

281 HTTP headers to include when sending requests. 

282 """ 

283 return self._headers 

284 

285 @headers.setter 

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

287 client_headers = Headers( 

288 { 

289 b"Accept": b"*/*", 

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

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

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

293 } 

294 ) 

295 client_headers.update(headers) 

296 self._headers = client_headers 

297 

298 @property 

299 def cookies(self) -> Cookies: 

300 """ 

301 Cookie values to include when sending requests. 

302 """ 

303 return self._cookies 

304 

305 @cookies.setter 

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

307 self._cookies = Cookies(cookies) 

308 

309 @property 

310 def params(self) -> QueryParams: 

311 """ 

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

313 """ 

314 return self._params 

315 

316 @params.setter 

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

318 self._params = QueryParams(params) 

319 

320 def build_request( 

321 self, 

322 method: str, 

323 url: URL | str, 

324 *, 

325 content: RequestContent | None = None, 

326 data: RequestData | None = None, 

327 files: RequestFiles | None = None, 

328 json: typing.Any | None = None, 

329 params: QueryParamTypes | None = None, 

330 headers: HeaderTypes | None = None, 

331 cookies: CookieTypes | None = None, 

332 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

333 extensions: RequestExtensions | None = None, 

334 ) -> Request: 

335 """ 

336 Build and return a request instance. 

337 

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

339 are merged with any values set on the client. 

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

341 

342 See also: [Request instances][0] 

343 

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

345 """ 

346 url = self._merge_url(url) 

347 headers = self._merge_headers(headers) 

348 cookies = self._merge_cookies(cookies) 

349 params = self._merge_queryparams(params) 

350 extensions = {} if extensions is None else extensions 

351 if "timeout" not in extensions: 

352 timeout = ( 

353 self.timeout 

354 if isinstance(timeout, UseClientDefault) 

355 else Timeout(timeout) 

356 ) 

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

358 return Request( 

359 method, 

360 url, 

361 content=content, 

362 data=data, 

363 files=files, 

364 json=json, 

365 params=params, 

366 headers=headers, 

367 cookies=cookies, 

368 extensions=extensions, 

369 ) 

370 

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

372 """ 

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

374 to create the URL used for the outgoing request. 

375 """ 

376 merge_url = URL(url) 

377 if merge_url.is_relative_url: 

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

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

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

381 # 

382 # So, eg... 

383 # 

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

385 # >>> client.base_url 

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

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

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

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

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

391 return merge_url 

392 

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

394 """ 

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

396 to create the cookies used for the outgoing request. 

397 """ 

398 if cookies or self.cookies: 

399 merged_cookies = Cookies(self.cookies) 

400 merged_cookies.update(cookies) 

401 return merged_cookies 

402 return cookies 

403 

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

405 """ 

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

407 to create the headers used for the outgoing request. 

408 """ 

409 merged_headers = Headers(self.headers) 

410 merged_headers.update(headers) 

411 return merged_headers 

412 

413 def _merge_queryparams( 

414 self, params: QueryParamTypes | None = None 

415 ) -> QueryParamTypes | None: 

416 """ 

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

418 to create the queryparams used for the outgoing request. 

419 """ 

420 if params or self.params: 

421 merged_queryparams = QueryParams(self.params) 

422 return merged_queryparams.merge(params) 

423 return params 

424 

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

426 if auth is None: 

427 return None 

428 elif isinstance(auth, tuple): 

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

430 elif isinstance(auth, Auth): 

431 return auth 

432 elif callable(auth): 

433 return FunctionAuth(func=auth) 

434 else: 

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

436 

437 def _build_request_auth( 

438 self, 

439 request: Request, 

440 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, 

441 ) -> Auth: 

442 auth = ( 

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

444 ) 

445 

446 if auth is not None: 

447 return auth 

448 

449 username, password = request.url.username, request.url.password 

450 if username or password: 

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

452 

453 return Auth() 

454 

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

456 """ 

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

458 should be used to effect the redirect. 

459 """ 

460 method = self._redirect_method(request, response) 

461 url = self._redirect_url(request, response) 

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

463 stream = self._redirect_stream(request, method) 

464 cookies = Cookies(self.cookies) 

465 return Request( 

466 method=method, 

467 url=url, 

468 headers=headers, 

469 cookies=cookies, 

470 stream=stream, 

471 extensions=request.extensions, 

472 ) 

473 

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

475 """ 

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

477 based on certain specs or browser behavior. 

478 """ 

479 method = request.method 

480 

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

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

483 method = "GET" 

484 

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

486 # Turn 302s into GETs. 

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

488 method = "GET" 

489 

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

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

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

493 method = "GET" 

494 

495 return method 

496 

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

498 """ 

499 Return the URL for the redirect to follow. 

500 """ 

501 location = response.headers["Location"] 

502 

503 try: 

504 url = URL(location) 

505 except InvalidURL as exc: 

506 raise RemoteProtocolError( 

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

508 ) from None 

509 

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

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

512 if url.scheme and not url.host: 

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

514 

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

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

517 if url.is_relative_url: 

518 url = request.url.join(url) 

519 

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

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

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

523 

524 return url 

525 

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

527 """ 

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

529 """ 

530 headers = Headers(request.headers) 

531 

532 if not same_origin(url, request.url): 

533 if not is_https_redirect(request.url, url): 

534 # Strip Authorization headers when responses are redirected 

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

536 headers.pop("Authorization", None) 

537 

538 # Update the Host header. 

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

540 

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

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

543 # are only relevant to the request body. 

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

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

546 

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

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

549 headers.pop("Cookie", None) 

550 

551 return headers 

552 

553 def _redirect_stream( 

554 self, request: Request, method: str 

555 ) -> SyncByteStream | AsyncByteStream | None: 

556 """ 

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

558 """ 

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

560 return None 

561 

562 return request.stream 

563 

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

565 if "timeout" not in request.extensions: 

566 timeout = ( 

567 self.timeout 

568 if isinstance(self.timeout, UseClientDefault) 

569 else Timeout(self.timeout) 

570 ) 

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

572 

573 

574class Client(BaseClient): 

575 """ 

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

577 

578 It can be shared between threads. 

579 

580 Usage: 

581 

582 ```python 

583 >>> client = httpx.Client() 

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

585 ``` 

586 

587 **Parameters:** 

588 

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

590 requests. 

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

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

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

594 sending requests. 

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

596 sending requests. 

597 * **verify** - *(optional)* SSL certificates (a.k.a CA bundle) used to 

598 verify the identity of requested hosts. Either `True` (default CA bundle), 

599 a path to an SSL certificate file, an `ssl.SSLContext`, or `False` 

600 (which will disable verification). 

601 * **cert** - *(optional)* An SSL certificate used by the requested host 

602 to authenticate the client. Either a path to an SSL certificate file, or 

603 two-tuple of (certificate file, key file), or a three-tuple of (certificate 

604 file, key file, password). 

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

606 enabled. Defaults to `False`. 

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

608 * **proxies** - *(optional)* A dictionary mapping proxy keys to proxy 

609 URLs. 

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

611 requests. 

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

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

614 that should be followed. 

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

616 request URLs. 

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

618 over the network. 

619 * **app** - *(optional)* An WSGI application to send requests to, 

620 rather than sending actual network requests. 

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

622 variables for configuration. 

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

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

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

626 """ 

627 

628 def __init__( 

629 self, 

630 *, 

631 auth: AuthTypes | None = None, 

632 params: QueryParamTypes | None = None, 

633 headers: HeaderTypes | None = None, 

634 cookies: CookieTypes | None = None, 

635 verify: VerifyTypes = True, 

636 cert: CertTypes | None = None, 

637 http1: bool = True, 

638 http2: bool = False, 

639 proxy: ProxyTypes | None = None, 

640 proxies: ProxiesTypes | None = None, 

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

642 timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, 

643 follow_redirects: bool = False, 

644 limits: Limits = DEFAULT_LIMITS, 

645 max_redirects: int = DEFAULT_MAX_REDIRECTS, 

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

647 base_url: URL | str = "", 

648 transport: BaseTransport | None = None, 

649 app: typing.Callable[..., typing.Any] | None = None, 

650 trust_env: bool = True, 

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

652 ) -> None: 

653 super().__init__( 

654 auth=auth, 

655 params=params, 

656 headers=headers, 

657 cookies=cookies, 

658 timeout=timeout, 

659 follow_redirects=follow_redirects, 

660 max_redirects=max_redirects, 

661 event_hooks=event_hooks, 

662 base_url=base_url, 

663 trust_env=trust_env, 

664 default_encoding=default_encoding, 

665 ) 

666 

667 if http2: 

668 try: 

669 import h2 # noqa 

670 except ImportError: # pragma: no cover 

671 raise ImportError( 

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

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

674 ) from None 

675 

676 if proxies: 

677 message = ( 

678 "The 'proxies' argument is now deprecated." 

679 " Use 'proxy' or 'mounts' instead." 

680 ) 

681 warnings.warn(message, DeprecationWarning) 

682 if proxy: 

683 raise RuntimeError("Use either `proxy` or 'proxies', not both.") 

684 

685 if app: 

686 message = ( 

687 "The 'app' shortcut is now deprecated." 

688 " Use the explicit style 'transport=WSGITransport(app=...)' instead." 

689 ) 

690 warnings.warn(message, DeprecationWarning) 

691 

692 allow_env_proxies = trust_env and app is None and transport is None 

693 proxy_map = self._get_proxy_map(proxies or proxy, allow_env_proxies) 

694 

695 self._transport = self._init_transport( 

696 verify=verify, 

697 cert=cert, 

698 http1=http1, 

699 http2=http2, 

700 limits=limits, 

701 transport=transport, 

702 app=app, 

703 trust_env=trust_env, 

704 ) 

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

706 URLPattern(key): None 

707 if proxy is None 

708 else self._init_proxy_transport( 

709 proxy, 

710 verify=verify, 

711 cert=cert, 

712 http1=http1, 

713 http2=http2, 

714 limits=limits, 

715 trust_env=trust_env, 

716 ) 

717 for key, proxy in proxy_map.items() 

718 } 

719 if mounts is not None: 

720 self._mounts.update( 

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

722 ) 

723 

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

725 

726 def _init_transport( 

727 self, 

728 verify: VerifyTypes = True, 

729 cert: CertTypes | None = None, 

730 http1: bool = True, 

731 http2: bool = False, 

732 limits: Limits = DEFAULT_LIMITS, 

733 transport: BaseTransport | None = None, 

734 app: typing.Callable[..., typing.Any] | None = None, 

735 trust_env: bool = True, 

736 ) -> BaseTransport: 

737 if transport is not None: 

738 return transport 

739 

740 if app is not None: 

741 return WSGITransport(app=app) 

742 

743 return HTTPTransport( 

744 verify=verify, 

745 cert=cert, 

746 http1=http1, 

747 http2=http2, 

748 limits=limits, 

749 trust_env=trust_env, 

750 ) 

751 

752 def _init_proxy_transport( 

753 self, 

754 proxy: Proxy, 

755 verify: VerifyTypes = True, 

756 cert: CertTypes | None = None, 

757 http1: bool = True, 

758 http2: bool = False, 

759 limits: Limits = DEFAULT_LIMITS, 

760 trust_env: bool = True, 

761 ) -> BaseTransport: 

762 return HTTPTransport( 

763 verify=verify, 

764 cert=cert, 

765 http1=http1, 

766 http2=http2, 

767 limits=limits, 

768 trust_env=trust_env, 

769 proxy=proxy, 

770 ) 

771 

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

773 """ 

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

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

776 """ 

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

778 if pattern.matches(url): 

779 return self._transport if transport is None else transport 

780 

781 return self._transport 

782 

783 def request( 

784 self, 

785 method: str, 

786 url: URL | str, 

787 *, 

788 content: RequestContent | None = None, 

789 data: RequestData | None = None, 

790 files: RequestFiles | None = None, 

791 json: typing.Any | None = None, 

792 params: QueryParamTypes | None = None, 

793 headers: HeaderTypes | None = None, 

794 cookies: CookieTypes | None = None, 

795 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, 

796 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

797 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

798 extensions: RequestExtensions | None = None, 

799 ) -> Response: 

800 """ 

801 Build and send a request. 

802 

803 Equivalent to: 

804 

805 ```python 

806 request = client.build_request(...) 

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

808 ``` 

809 

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

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

812 are merged with client-level configuration. 

813 

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

815 """ 

816 if cookies is not None: 

817 message = ( 

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

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

820 "cookies directly on the client instance instead." 

821 ) 

822 warnings.warn(message, DeprecationWarning) 

823 

824 request = self.build_request( 

825 method=method, 

826 url=url, 

827 content=content, 

828 data=data, 

829 files=files, 

830 json=json, 

831 params=params, 

832 headers=headers, 

833 cookies=cookies, 

834 timeout=timeout, 

835 extensions=extensions, 

836 ) 

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

838 

839 @contextmanager 

840 def stream( 

841 self, 

842 method: str, 

843 url: URL | str, 

844 *, 

845 content: RequestContent | None = None, 

846 data: RequestData | None = None, 

847 files: RequestFiles | None = None, 

848 json: typing.Any | None = None, 

849 params: QueryParamTypes | None = None, 

850 headers: HeaderTypes | None = None, 

851 cookies: CookieTypes | None = None, 

852 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, 

853 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

854 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

855 extensions: RequestExtensions | None = None, 

856 ) -> typing.Iterator[Response]: 

857 """ 

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

859 instead of loading it into memory at once. 

860 

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

862 

863 See also: [Streaming Responses][0] 

864 

865 [0]: /quickstart#streaming-responses 

866 """ 

867 request = self.build_request( 

868 method=method, 

869 url=url, 

870 content=content, 

871 data=data, 

872 files=files, 

873 json=json, 

874 params=params, 

875 headers=headers, 

876 cookies=cookies, 

877 timeout=timeout, 

878 extensions=extensions, 

879 ) 

880 response = self.send( 

881 request=request, 

882 auth=auth, 

883 follow_redirects=follow_redirects, 

884 stream=True, 

885 ) 

886 try: 

887 yield response 

888 finally: 

889 response.close() 

890 

891 def send( 

892 self, 

893 request: Request, 

894 *, 

895 stream: bool = False, 

896 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, 

897 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

898 ) -> Response: 

899 """ 

900 Send a request. 

901 

902 The request is sent as-is, unmodified. 

903 

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

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

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

907 

908 See also: [Request instances][0] 

909 

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

911 """ 

912 if self._state == ClientState.CLOSED: 

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

914 

915 self._state = ClientState.OPENED 

916 follow_redirects = ( 

917 self.follow_redirects 

918 if isinstance(follow_redirects, UseClientDefault) 

919 else follow_redirects 

920 ) 

921 

922 self._set_timeout(request) 

923 

924 auth = self._build_request_auth(request, auth) 

925 

926 response = self._send_handling_auth( 

927 request, 

928 auth=auth, 

929 follow_redirects=follow_redirects, 

930 history=[], 

931 ) 

932 try: 

933 if not stream: 

934 response.read() 

935 

936 return response 

937 

938 except BaseException as exc: 

939 response.close() 

940 raise exc 

941 

942 def _send_handling_auth( 

943 self, 

944 request: Request, 

945 auth: Auth, 

946 follow_redirects: bool, 

947 history: list[Response], 

948 ) -> Response: 

949 auth_flow = auth.sync_auth_flow(request) 

950 try: 

951 request = next(auth_flow) 

952 

953 while True: 

954 response = self._send_handling_redirects( 

955 request, 

956 follow_redirects=follow_redirects, 

957 history=history, 

958 ) 

959 try: 

960 try: 

961 next_request = auth_flow.send(response) 

962 except StopIteration: 

963 return response 

964 

965 response.history = list(history) 

966 response.read() 

967 request = next_request 

968 history.append(response) 

969 

970 except BaseException as exc: 

971 response.close() 

972 raise exc 

973 finally: 

974 auth_flow.close() 

975 

976 def _send_handling_redirects( 

977 self, 

978 request: Request, 

979 follow_redirects: bool, 

980 history: list[Response], 

981 ) -> Response: 

982 while True: 

983 if len(history) > self.max_redirects: 

984 raise TooManyRedirects( 

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

986 ) 

987 

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

989 hook(request) 

990 

991 response = self._send_single_request(request) 

992 try: 

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

994 hook(response) 

995 response.history = list(history) 

996 

997 if not response.has_redirect_location: 

998 return response 

999 

1000 request = self._build_redirect_request(request, response) 

1001 history = history + [response] 

1002 

1003 if follow_redirects: 

1004 response.read() 

1005 else: 

1006 response.next_request = request 

1007 return response 

1008 

1009 except BaseException as exc: 

1010 response.close() 

1011 raise exc 

1012 

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

1014 """ 

1015 Sends a single request, without handling any redirections. 

1016 """ 

1017 transport = self._transport_for_url(request.url) 

1018 timer = Timer() 

1019 timer.sync_start() 

1020 

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

1022 raise RuntimeError( 

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

1024 ) 

1025 

1026 with request_context(request=request): 

1027 response = transport.handle_request(request) 

1028 

1029 assert isinstance(response.stream, SyncByteStream) 

1030 

1031 response.request = request 

1032 response.stream = BoundSyncStream( 

1033 response.stream, response=response, timer=timer 

1034 ) 

1035 self.cookies.extract_cookies(response) 

1036 response.default_encoding = self._default_encoding 

1037 

1038 logger.info( 

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

1040 request.method, 

1041 request.url, 

1042 response.http_version, 

1043 response.status_code, 

1044 response.reason_phrase, 

1045 ) 

1046 

1047 return response 

1048 

1049 def get( 

1050 self, 

1051 url: URL | str, 

1052 *, 

1053 params: QueryParamTypes | None = None, 

1054 headers: HeaderTypes | None = None, 

1055 cookies: CookieTypes | None = None, 

1056 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, 

1057 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1058 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1059 extensions: RequestExtensions | None = None, 

1060 ) -> Response: 

1061 """ 

1062 Send a `GET` request. 

1063 

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

1065 """ 

1066 return self.request( 

1067 "GET", 

1068 url, 

1069 params=params, 

1070 headers=headers, 

1071 cookies=cookies, 

1072 auth=auth, 

1073 follow_redirects=follow_redirects, 

1074 timeout=timeout, 

1075 extensions=extensions, 

1076 ) 

1077 

1078 def options( 

1079 self, 

1080 url: URL | str, 

1081 *, 

1082 params: QueryParamTypes | None = None, 

1083 headers: HeaderTypes | None = None, 

1084 cookies: CookieTypes | None = None, 

1085 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1086 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1087 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1088 extensions: RequestExtensions | None = None, 

1089 ) -> Response: 

1090 """ 

1091 Send an `OPTIONS` request. 

1092 

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

1094 """ 

1095 return self.request( 

1096 "OPTIONS", 

1097 url, 

1098 params=params, 

1099 headers=headers, 

1100 cookies=cookies, 

1101 auth=auth, 

1102 follow_redirects=follow_redirects, 

1103 timeout=timeout, 

1104 extensions=extensions, 

1105 ) 

1106 

1107 def head( 

1108 self, 

1109 url: URL | str, 

1110 *, 

1111 params: QueryParamTypes | None = None, 

1112 headers: HeaderTypes | None = None, 

1113 cookies: CookieTypes | None = None, 

1114 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1115 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1116 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1117 extensions: RequestExtensions | None = None, 

1118 ) -> Response: 

1119 """ 

1120 Send a `HEAD` request. 

1121 

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

1123 """ 

1124 return self.request( 

1125 "HEAD", 

1126 url, 

1127 params=params, 

1128 headers=headers, 

1129 cookies=cookies, 

1130 auth=auth, 

1131 follow_redirects=follow_redirects, 

1132 timeout=timeout, 

1133 extensions=extensions, 

1134 ) 

1135 

1136 def post( 

1137 self, 

1138 url: URL | str, 

1139 *, 

1140 content: RequestContent | None = None, 

1141 data: RequestData | None = None, 

1142 files: RequestFiles | None = None, 

1143 json: typing.Any | None = None, 

1144 params: QueryParamTypes | None = None, 

1145 headers: HeaderTypes | None = None, 

1146 cookies: CookieTypes | None = None, 

1147 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1148 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1149 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1150 extensions: RequestExtensions | None = None, 

1151 ) -> Response: 

1152 """ 

1153 Send a `POST` request. 

1154 

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

1156 """ 

1157 return self.request( 

1158 "POST", 

1159 url, 

1160 content=content, 

1161 data=data, 

1162 files=files, 

1163 json=json, 

1164 params=params, 

1165 headers=headers, 

1166 cookies=cookies, 

1167 auth=auth, 

1168 follow_redirects=follow_redirects, 

1169 timeout=timeout, 

1170 extensions=extensions, 

1171 ) 

1172 

1173 def put( 

1174 self, 

1175 url: URL | str, 

1176 *, 

1177 content: RequestContent | None = None, 

1178 data: RequestData | None = None, 

1179 files: RequestFiles | None = None, 

1180 json: typing.Any | None = None, 

1181 params: QueryParamTypes | None = None, 

1182 headers: HeaderTypes | None = None, 

1183 cookies: CookieTypes | None = None, 

1184 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1185 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1186 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1187 extensions: RequestExtensions | None = None, 

1188 ) -> Response: 

1189 """ 

1190 Send a `PUT` request. 

1191 

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

1193 """ 

1194 return self.request( 

1195 "PUT", 

1196 url, 

1197 content=content, 

1198 data=data, 

1199 files=files, 

1200 json=json, 

1201 params=params, 

1202 headers=headers, 

1203 cookies=cookies, 

1204 auth=auth, 

1205 follow_redirects=follow_redirects, 

1206 timeout=timeout, 

1207 extensions=extensions, 

1208 ) 

1209 

1210 def patch( 

1211 self, 

1212 url: URL | str, 

1213 *, 

1214 content: RequestContent | None = None, 

1215 data: RequestData | None = None, 

1216 files: RequestFiles | None = None, 

1217 json: typing.Any | None = None, 

1218 params: QueryParamTypes | None = None, 

1219 headers: HeaderTypes | None = None, 

1220 cookies: CookieTypes | None = None, 

1221 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1222 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1223 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1224 extensions: RequestExtensions | None = None, 

1225 ) -> Response: 

1226 """ 

1227 Send a `PATCH` request. 

1228 

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

1230 """ 

1231 return self.request( 

1232 "PATCH", 

1233 url, 

1234 content=content, 

1235 data=data, 

1236 files=files, 

1237 json=json, 

1238 params=params, 

1239 headers=headers, 

1240 cookies=cookies, 

1241 auth=auth, 

1242 follow_redirects=follow_redirects, 

1243 timeout=timeout, 

1244 extensions=extensions, 

1245 ) 

1246 

1247 def delete( 

1248 self, 

1249 url: URL | str, 

1250 *, 

1251 params: QueryParamTypes | None = None, 

1252 headers: HeaderTypes | None = None, 

1253 cookies: CookieTypes | None = None, 

1254 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1255 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1256 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1257 extensions: RequestExtensions | None = None, 

1258 ) -> Response: 

1259 """ 

1260 Send a `DELETE` request. 

1261 

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

1263 """ 

1264 return self.request( 

1265 "DELETE", 

1266 url, 

1267 params=params, 

1268 headers=headers, 

1269 cookies=cookies, 

1270 auth=auth, 

1271 follow_redirects=follow_redirects, 

1272 timeout=timeout, 

1273 extensions=extensions, 

1274 ) 

1275 

1276 def close(self) -> None: 

1277 """ 

1278 Close transport and proxies. 

1279 """ 

1280 if self._state != ClientState.CLOSED: 

1281 self._state = ClientState.CLOSED 

1282 

1283 self._transport.close() 

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

1285 if transport is not None: 

1286 transport.close() 

1287 

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

1289 if self._state != ClientState.UNOPENED: 

1290 msg = { 

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

1292 ClientState.CLOSED: ( 

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

1294 ), 

1295 }[self._state] 

1296 raise RuntimeError(msg) 

1297 

1298 self._state = ClientState.OPENED 

1299 

1300 self._transport.__enter__() 

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

1302 if transport is not None: 

1303 transport.__enter__() 

1304 return self 

1305 

1306 def __exit__( 

1307 self, 

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

1309 exc_value: BaseException | None = None, 

1310 traceback: TracebackType | None = None, 

1311 ) -> None: 

1312 self._state = ClientState.CLOSED 

1313 

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

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

1316 if transport is not None: 

1317 transport.__exit__(exc_type, exc_value, traceback) 

1318 

1319 

1320class AsyncClient(BaseClient): 

1321 """ 

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

1323 cookie persistence, etc. 

1324 

1325 It can be shared between tasks. 

1326 

1327 Usage: 

1328 

1329 ```python 

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

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

1332 ``` 

1333 

1334 **Parameters:** 

1335 

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

1337 requests. 

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

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

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

1341 sending requests. 

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

1343 sending requests. 

1344 * **verify** - *(optional)* SSL certificates (a.k.a CA bundle) used to 

1345 verify the identity of requested hosts. Either `True` (default CA bundle), 

1346 a path to an SSL certificate file, an `ssl.SSLContext`, or `False` 

1347 (which will disable verification). 

1348 * **cert** - *(optional)* An SSL certificate used by the requested host 

1349 to authenticate the client. Either a path to an SSL certificate file, or 

1350 two-tuple of (certificate file, key file), or a three-tuple of (certificate 

1351 file, key file, password). 

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

1353 enabled. Defaults to `False`. 

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

1355 * **proxies** - *(optional)* A dictionary mapping HTTP protocols to proxy 

1356 URLs. 

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

1358 requests. 

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

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

1361 that should be followed. 

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

1363 request URLs. 

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

1365 over the network. 

1366 * **app** - *(optional)* An ASGI application to send requests to, 

1367 rather than sending actual network requests. 

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

1369 variables for configuration. 

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

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

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

1373 """ 

1374 

1375 def __init__( 

1376 self, 

1377 *, 

1378 auth: AuthTypes | None = None, 

1379 params: QueryParamTypes | None = None, 

1380 headers: HeaderTypes | None = None, 

1381 cookies: CookieTypes | None = None, 

1382 verify: VerifyTypes = True, 

1383 cert: CertTypes | None = None, 

1384 http1: bool = True, 

1385 http2: bool = False, 

1386 proxy: ProxyTypes | None = None, 

1387 proxies: ProxiesTypes | None = None, 

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

1389 timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, 

1390 follow_redirects: bool = False, 

1391 limits: Limits = DEFAULT_LIMITS, 

1392 max_redirects: int = DEFAULT_MAX_REDIRECTS, 

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

1394 base_url: URL | str = "", 

1395 transport: AsyncBaseTransport | None = None, 

1396 app: typing.Callable[..., typing.Any] | None = None, 

1397 trust_env: bool = True, 

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

1399 ) -> None: 

1400 super().__init__( 

1401 auth=auth, 

1402 params=params, 

1403 headers=headers, 

1404 cookies=cookies, 

1405 timeout=timeout, 

1406 follow_redirects=follow_redirects, 

1407 max_redirects=max_redirects, 

1408 event_hooks=event_hooks, 

1409 base_url=base_url, 

1410 trust_env=trust_env, 

1411 default_encoding=default_encoding, 

1412 ) 

1413 

1414 if http2: 

1415 try: 

1416 import h2 # noqa 

1417 except ImportError: # pragma: no cover 

1418 raise ImportError( 

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

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

1421 ) from None 

1422 

1423 if proxies: 

1424 message = ( 

1425 "The 'proxies' argument is now deprecated." 

1426 " Use 'proxy' or 'mounts' instead." 

1427 ) 

1428 warnings.warn(message, DeprecationWarning) 

1429 if proxy: 

1430 raise RuntimeError("Use either `proxy` or 'proxies', not both.") 

1431 

1432 if app: 

1433 message = ( 

1434 "The 'app' shortcut is now deprecated." 

1435 " Use the explicit style 'transport=ASGITransport(app=...)' instead." 

1436 ) 

1437 warnings.warn(message, DeprecationWarning) 

1438 

1439 allow_env_proxies = trust_env and app is None and transport is None 

1440 proxy_map = self._get_proxy_map(proxies or proxy, allow_env_proxies) 

1441 

1442 self._transport = self._init_transport( 

1443 verify=verify, 

1444 cert=cert, 

1445 http1=http1, 

1446 http2=http2, 

1447 limits=limits, 

1448 transport=transport, 

1449 app=app, 

1450 trust_env=trust_env, 

1451 ) 

1452 

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

1454 URLPattern(key): None 

1455 if proxy is None 

1456 else self._init_proxy_transport( 

1457 proxy, 

1458 verify=verify, 

1459 cert=cert, 

1460 http1=http1, 

1461 http2=http2, 

1462 limits=limits, 

1463 trust_env=trust_env, 

1464 ) 

1465 for key, proxy in proxy_map.items() 

1466 } 

1467 if mounts is not None: 

1468 self._mounts.update( 

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

1470 ) 

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

1472 

1473 def _init_transport( 

1474 self, 

1475 verify: VerifyTypes = True, 

1476 cert: CertTypes | None = None, 

1477 http1: bool = True, 

1478 http2: bool = False, 

1479 limits: Limits = DEFAULT_LIMITS, 

1480 transport: AsyncBaseTransport | None = None, 

1481 app: typing.Callable[..., typing.Any] | None = None, 

1482 trust_env: bool = True, 

1483 ) -> AsyncBaseTransport: 

1484 if transport is not None: 

1485 return transport 

1486 

1487 if app is not None: 

1488 return ASGITransport(app=app) 

1489 

1490 return AsyncHTTPTransport( 

1491 verify=verify, 

1492 cert=cert, 

1493 http1=http1, 

1494 http2=http2, 

1495 limits=limits, 

1496 trust_env=trust_env, 

1497 ) 

1498 

1499 def _init_proxy_transport( 

1500 self, 

1501 proxy: Proxy, 

1502 verify: VerifyTypes = True, 

1503 cert: CertTypes | None = None, 

1504 http1: bool = True, 

1505 http2: bool = False, 

1506 limits: Limits = DEFAULT_LIMITS, 

1507 trust_env: bool = True, 

1508 ) -> AsyncBaseTransport: 

1509 return AsyncHTTPTransport( 

1510 verify=verify, 

1511 cert=cert, 

1512 http1=http1, 

1513 http2=http2, 

1514 limits=limits, 

1515 trust_env=trust_env, 

1516 proxy=proxy, 

1517 ) 

1518 

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

1520 """ 

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

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

1523 """ 

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

1525 if pattern.matches(url): 

1526 return self._transport if transport is None else transport 

1527 

1528 return self._transport 

1529 

1530 async def request( 

1531 self, 

1532 method: str, 

1533 url: URL | str, 

1534 *, 

1535 content: RequestContent | None = None, 

1536 data: RequestData | None = None, 

1537 files: RequestFiles | None = None, 

1538 json: typing.Any | None = None, 

1539 params: QueryParamTypes | None = None, 

1540 headers: HeaderTypes | None = None, 

1541 cookies: CookieTypes | None = None, 

1542 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, 

1543 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1544 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1545 extensions: RequestExtensions | None = None, 

1546 ) -> Response: 

1547 """ 

1548 Build and send a request. 

1549 

1550 Equivalent to: 

1551 

1552 ```python 

1553 request = client.build_request(...) 

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

1555 ``` 

1556 

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

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

1559 are merged with client-level configuration. 

1560 

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

1562 """ 

1563 

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

1565 message = ( 

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

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

1568 "cookies directly on the client instance instead." 

1569 ) 

1570 warnings.warn(message, DeprecationWarning) 

1571 

1572 request = self.build_request( 

1573 method=method, 

1574 url=url, 

1575 content=content, 

1576 data=data, 

1577 files=files, 

1578 json=json, 

1579 params=params, 

1580 headers=headers, 

1581 cookies=cookies, 

1582 timeout=timeout, 

1583 extensions=extensions, 

1584 ) 

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

1586 

1587 @asynccontextmanager 

1588 async def stream( 

1589 self, 

1590 method: str, 

1591 url: URL | str, 

1592 *, 

1593 content: RequestContent | None = None, 

1594 data: RequestData | None = None, 

1595 files: RequestFiles | None = None, 

1596 json: typing.Any | None = None, 

1597 params: QueryParamTypes | None = None, 

1598 headers: HeaderTypes | None = None, 

1599 cookies: CookieTypes | None = None, 

1600 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, 

1601 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1602 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1603 extensions: RequestExtensions | None = None, 

1604 ) -> typing.AsyncIterator[Response]: 

1605 """ 

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

1607 instead of loading it into memory at once. 

1608 

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

1610 

1611 See also: [Streaming Responses][0] 

1612 

1613 [0]: /quickstart#streaming-responses 

1614 """ 

1615 request = self.build_request( 

1616 method=method, 

1617 url=url, 

1618 content=content, 

1619 data=data, 

1620 files=files, 

1621 json=json, 

1622 params=params, 

1623 headers=headers, 

1624 cookies=cookies, 

1625 timeout=timeout, 

1626 extensions=extensions, 

1627 ) 

1628 response = await self.send( 

1629 request=request, 

1630 auth=auth, 

1631 follow_redirects=follow_redirects, 

1632 stream=True, 

1633 ) 

1634 try: 

1635 yield response 

1636 finally: 

1637 await response.aclose() 

1638 

1639 async def send( 

1640 self, 

1641 request: Request, 

1642 *, 

1643 stream: bool = False, 

1644 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, 

1645 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1646 ) -> Response: 

1647 """ 

1648 Send a request. 

1649 

1650 The request is sent as-is, unmodified. 

1651 

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

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

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

1655 

1656 See also: [Request instances][0] 

1657 

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

1659 """ 

1660 if self._state == ClientState.CLOSED: 

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

1662 

1663 self._state = ClientState.OPENED 

1664 follow_redirects = ( 

1665 self.follow_redirects 

1666 if isinstance(follow_redirects, UseClientDefault) 

1667 else follow_redirects 

1668 ) 

1669 

1670 self._set_timeout(request) 

1671 

1672 auth = self._build_request_auth(request, auth) 

1673 

1674 response = await self._send_handling_auth( 

1675 request, 

1676 auth=auth, 

1677 follow_redirects=follow_redirects, 

1678 history=[], 

1679 ) 

1680 try: 

1681 if not stream: 

1682 await response.aread() 

1683 

1684 return response 

1685 

1686 except BaseException as exc: 

1687 await response.aclose() 

1688 raise exc 

1689 

1690 async def _send_handling_auth( 

1691 self, 

1692 request: Request, 

1693 auth: Auth, 

1694 follow_redirects: bool, 

1695 history: list[Response], 

1696 ) -> Response: 

1697 auth_flow = auth.async_auth_flow(request) 

1698 try: 

1699 request = await auth_flow.__anext__() 

1700 

1701 while True: 

1702 response = await self._send_handling_redirects( 

1703 request, 

1704 follow_redirects=follow_redirects, 

1705 history=history, 

1706 ) 

1707 try: 

1708 try: 

1709 next_request = await auth_flow.asend(response) 

1710 except StopAsyncIteration: 

1711 return response 

1712 

1713 response.history = list(history) 

1714 await response.aread() 

1715 request = next_request 

1716 history.append(response) 

1717 

1718 except BaseException as exc: 

1719 await response.aclose() 

1720 raise exc 

1721 finally: 

1722 await auth_flow.aclose() 

1723 

1724 async def _send_handling_redirects( 

1725 self, 

1726 request: Request, 

1727 follow_redirects: bool, 

1728 history: list[Response], 

1729 ) -> Response: 

1730 while True: 

1731 if len(history) > self.max_redirects: 

1732 raise TooManyRedirects( 

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

1734 ) 

1735 

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

1737 await hook(request) 

1738 

1739 response = await self._send_single_request(request) 

1740 try: 

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

1742 await hook(response) 

1743 

1744 response.history = list(history) 

1745 

1746 if not response.has_redirect_location: 

1747 return response 

1748 

1749 request = self._build_redirect_request(request, response) 

1750 history = history + [response] 

1751 

1752 if follow_redirects: 

1753 await response.aread() 

1754 else: 

1755 response.next_request = request 

1756 return response 

1757 

1758 except BaseException as exc: 

1759 await response.aclose() 

1760 raise exc 

1761 

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

1763 """ 

1764 Sends a single request, without handling any redirections. 

1765 """ 

1766 transport = self._transport_for_url(request.url) 

1767 timer = Timer() 

1768 await timer.async_start() 

1769 

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

1771 raise RuntimeError( 

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

1773 ) 

1774 

1775 with request_context(request=request): 

1776 response = await transport.handle_async_request(request) 

1777 

1778 assert isinstance(response.stream, AsyncByteStream) 

1779 response.request = request 

1780 response.stream = BoundAsyncStream( 

1781 response.stream, response=response, timer=timer 

1782 ) 

1783 self.cookies.extract_cookies(response) 

1784 response.default_encoding = self._default_encoding 

1785 

1786 logger.info( 

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

1788 request.method, 

1789 request.url, 

1790 response.http_version, 

1791 response.status_code, 

1792 response.reason_phrase, 

1793 ) 

1794 

1795 return response 

1796 

1797 async def get( 

1798 self, 

1799 url: URL | str, 

1800 *, 

1801 params: QueryParamTypes | None = None, 

1802 headers: HeaderTypes | None = None, 

1803 cookies: CookieTypes | None = None, 

1804 auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, 

1805 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1806 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1807 extensions: RequestExtensions | None = None, 

1808 ) -> Response: 

1809 """ 

1810 Send a `GET` request. 

1811 

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

1813 """ 

1814 return await self.request( 

1815 "GET", 

1816 url, 

1817 params=params, 

1818 headers=headers, 

1819 cookies=cookies, 

1820 auth=auth, 

1821 follow_redirects=follow_redirects, 

1822 timeout=timeout, 

1823 extensions=extensions, 

1824 ) 

1825 

1826 async def options( 

1827 self, 

1828 url: URL | str, 

1829 *, 

1830 params: QueryParamTypes | None = None, 

1831 headers: HeaderTypes | None = None, 

1832 cookies: CookieTypes | None = None, 

1833 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1834 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1835 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1836 extensions: RequestExtensions | None = None, 

1837 ) -> Response: 

1838 """ 

1839 Send an `OPTIONS` request. 

1840 

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

1842 """ 

1843 return await self.request( 

1844 "OPTIONS", 

1845 url, 

1846 params=params, 

1847 headers=headers, 

1848 cookies=cookies, 

1849 auth=auth, 

1850 follow_redirects=follow_redirects, 

1851 timeout=timeout, 

1852 extensions=extensions, 

1853 ) 

1854 

1855 async def head( 

1856 self, 

1857 url: URL | str, 

1858 *, 

1859 params: QueryParamTypes | None = None, 

1860 headers: HeaderTypes | None = None, 

1861 cookies: CookieTypes | None = None, 

1862 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1863 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1864 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1865 extensions: RequestExtensions | None = None, 

1866 ) -> Response: 

1867 """ 

1868 Send a `HEAD` request. 

1869 

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

1871 """ 

1872 return await self.request( 

1873 "HEAD", 

1874 url, 

1875 params=params, 

1876 headers=headers, 

1877 cookies=cookies, 

1878 auth=auth, 

1879 follow_redirects=follow_redirects, 

1880 timeout=timeout, 

1881 extensions=extensions, 

1882 ) 

1883 

1884 async def post( 

1885 self, 

1886 url: URL | str, 

1887 *, 

1888 content: RequestContent | None = None, 

1889 data: RequestData | None = None, 

1890 files: RequestFiles | None = None, 

1891 json: typing.Any | None = None, 

1892 params: QueryParamTypes | None = None, 

1893 headers: HeaderTypes | None = None, 

1894 cookies: CookieTypes | None = None, 

1895 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1896 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1897 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1898 extensions: RequestExtensions | None = None, 

1899 ) -> Response: 

1900 """ 

1901 Send a `POST` request. 

1902 

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

1904 """ 

1905 return await self.request( 

1906 "POST", 

1907 url, 

1908 content=content, 

1909 data=data, 

1910 files=files, 

1911 json=json, 

1912 params=params, 

1913 headers=headers, 

1914 cookies=cookies, 

1915 auth=auth, 

1916 follow_redirects=follow_redirects, 

1917 timeout=timeout, 

1918 extensions=extensions, 

1919 ) 

1920 

1921 async def put( 

1922 self, 

1923 url: URL | str, 

1924 *, 

1925 content: RequestContent | None = None, 

1926 data: RequestData | None = None, 

1927 files: RequestFiles | None = None, 

1928 json: typing.Any | None = None, 

1929 params: QueryParamTypes | None = None, 

1930 headers: HeaderTypes | None = None, 

1931 cookies: CookieTypes | None = None, 

1932 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1933 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1934 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1935 extensions: RequestExtensions | None = None, 

1936 ) -> Response: 

1937 """ 

1938 Send a `PUT` request. 

1939 

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

1941 """ 

1942 return await self.request( 

1943 "PUT", 

1944 url, 

1945 content=content, 

1946 data=data, 

1947 files=files, 

1948 json=json, 

1949 params=params, 

1950 headers=headers, 

1951 cookies=cookies, 

1952 auth=auth, 

1953 follow_redirects=follow_redirects, 

1954 timeout=timeout, 

1955 extensions=extensions, 

1956 ) 

1957 

1958 async def patch( 

1959 self, 

1960 url: URL | str, 

1961 *, 

1962 content: RequestContent | None = None, 

1963 data: RequestData | None = None, 

1964 files: RequestFiles | None = None, 

1965 json: typing.Any | None = None, 

1966 params: QueryParamTypes | None = None, 

1967 headers: HeaderTypes | None = None, 

1968 cookies: CookieTypes | None = None, 

1969 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1970 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

1971 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

1972 extensions: RequestExtensions | None = None, 

1973 ) -> Response: 

1974 """ 

1975 Send a `PATCH` request. 

1976 

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

1978 """ 

1979 return await self.request( 

1980 "PATCH", 

1981 url, 

1982 content=content, 

1983 data=data, 

1984 files=files, 

1985 json=json, 

1986 params=params, 

1987 headers=headers, 

1988 cookies=cookies, 

1989 auth=auth, 

1990 follow_redirects=follow_redirects, 

1991 timeout=timeout, 

1992 extensions=extensions, 

1993 ) 

1994 

1995 async def delete( 

1996 self, 

1997 url: URL | str, 

1998 *, 

1999 params: QueryParamTypes | None = None, 

2000 headers: HeaderTypes | None = None, 

2001 cookies: CookieTypes | None = None, 

2002 auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

2003 follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, 

2004 timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, 

2005 extensions: RequestExtensions | None = None, 

2006 ) -> Response: 

2007 """ 

2008 Send a `DELETE` request. 

2009 

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

2011 """ 

2012 return await self.request( 

2013 "DELETE", 

2014 url, 

2015 params=params, 

2016 headers=headers, 

2017 cookies=cookies, 

2018 auth=auth, 

2019 follow_redirects=follow_redirects, 

2020 timeout=timeout, 

2021 extensions=extensions, 

2022 ) 

2023 

2024 async def aclose(self) -> None: 

2025 """ 

2026 Close transport and proxies. 

2027 """ 

2028 if self._state != ClientState.CLOSED: 

2029 self._state = ClientState.CLOSED 

2030 

2031 await self._transport.aclose() 

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

2033 if proxy is not None: 

2034 await proxy.aclose() 

2035 

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

2037 if self._state != ClientState.UNOPENED: 

2038 msg = { 

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

2040 ClientState.CLOSED: ( 

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

2042 ), 

2043 }[self._state] 

2044 raise RuntimeError(msg) 

2045 

2046 self._state = ClientState.OPENED 

2047 

2048 await self._transport.__aenter__() 

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

2050 if proxy is not None: 

2051 await proxy.__aenter__() 

2052 return self 

2053 

2054 async def __aexit__( 

2055 self, 

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

2057 exc_value: BaseException | None = None, 

2058 traceback: TracebackType | None = None, 

2059 ) -> None: 

2060 self._state = ClientState.CLOSED 

2061 

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

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

2064 if proxy is not None: 

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