← requests / src/requests/sessions.py
| 1 | """ |
| 2 | requests.sessions |
| 3 | ~~~~~~~~~~~~~~~~~ |
| 4 | |
| 5 | This module provides a Session object to manage and persist settings across |
| 6 | requests (cookies, auth, proxies). |
| 7 | """ |
| 8 | |
| 9 | from __future__ import annotations |
| 10 | |
| 11 | import os |
| 12 | import sys |
| 13 | import time |
| 14 | from collections import OrderedDict |
| 15 | from collections.abc import Generator, Mapping, MutableMapping |
| 16 | from datetime import timedelta |
| 17 | from typing import TYPE_CHECKING, Any, cast |
| 18 | |
| 19 | from ._internal_utils import to_native_string |
| 20 | from ._types import is_prepared as _is_prepared |
| 21 | from .adapters import HTTPAdapter |
| 22 | from .auth import _basic_auth_str # type: ignore[reportPrivateUsage] |
| 23 | from .compat import cookielib, urljoin, urlparse |
| 24 | from .cookies import ( |
| 25 | RequestsCookieJar, |
| 26 | cookiejar_from_dict, |
| 27 | extract_cookies_to_jar, |
| 28 | merge_cookies, |
| 29 | ) |
| 30 | from .exceptions import ( |
| 31 | ChunkedEncodingError, |
| 32 | ContentDecodingError, |
| 33 | InvalidSchema, |
| 34 | TooManyRedirects, |
| 35 | ) |
| 36 | from .hooks import default_hooks, dispatch_hook |
| 37 | |
| 38 | # formerly defined here, reexposed here for backward compatibility |
| 39 | from .models import ( # noqa: F401 |
| 40 | DEFAULT_REDIRECT_LIMIT, |
| 41 | REDIRECT_STATI, # type: ignore[reportUnusedImport] |
| 42 | PreparedRequest, |
| 43 | Request, |
| 44 | Response, |
| 45 | ) |
| 46 | from .status_codes import codes |
| 47 | from .structures import CaseInsensitiveDict |
| 48 | from .utils import ( # noqa: F401 |
| 49 | DEFAULT_PORTS, |
| 50 | default_headers, |
| 51 | get_auth_from_url, |
| 52 | get_environ_proxies, |
| 53 | get_netrc_auth, |
| 54 | requote_uri, |
| 55 | resolve_proxies, |
| 56 | rewind_body, |
| 57 | should_bypass_proxies, # type: ignore[reportUnusedImport] # re-export for external consumers |
| 58 | to_key_val_list, |
| 59 | ) |
| 60 | |
| 61 | if TYPE_CHECKING: |
| 62 | from http.cookiejar import CookieJar |
| 63 | |
| 64 | from typing_extensions import Self, Unpack |
| 65 | |
| 66 | from . import _types as _t |
| 67 | from .adapters import BaseAdapter |
| 68 | |
| 69 | # Preferred clock, based on which one is more accurate on a given system. |
| 70 | if sys.platform == "win32": |
| 71 | preferred_clock = time.perf_counter |
| 72 | else: |
| 73 | preferred_clock = time.time |
| 74 | |
| 75 | |
| 76 | def merge_setting( |
| 77 | request_setting: Any, session_setting: Any, dict_class: type = OrderedDict |
| 78 | ) -> Any: |
| 79 | """Determines appropriate setting for a given request, taking into account |
| 80 | the explicit setting on that request, and the setting in the session. If a |
| 81 | setting is a dictionary, they will be merged together using `dict_class` |
| 82 | """ |
| 83 | |
| 84 | if session_setting is None: |
| 85 | return request_setting |
| 86 | |
| 87 | if request_setting is None: |
| 88 | return session_setting |
| 89 | |
| 90 | # Bypass if not a dictionary (e.g. verify) |
| 91 | if not ( |
| 92 | isinstance(session_setting, Mapping) and isinstance(request_setting, Mapping) |
| 93 | ): |
| 94 | return request_setting |
| 95 | |
| 96 | merged_setting = dict_class(to_key_val_list(session_setting)) # type: ignore[arg-type] # isinstance narrows Any to Mapping[Unknown] |
| 97 | merged_setting.update(to_key_val_list(request_setting)) # type: ignore[arg-type] |
| 98 | |
| 99 | # Remove keys that are set to None. Extract keys first to avoid altering |
| 100 | # the dictionary during iteration. |
| 101 | none_keys = [k for (k, v) in merged_setting.items() if v is None] |
| 102 | for key in none_keys: |
| 103 | del merged_setting[key] |
| 104 | |
| 105 | return merged_setting |
| 106 | |
| 107 | |
| 108 | def merge_hooks( |
| 109 | request_hooks: _t.HooksType, |
| 110 | session_hooks: _t.HooksType, |
| 111 | dict_class: type = OrderedDict, |
| 112 | ) -> _t.HooksType: |
| 113 | """Properly merges both requests and session hooks. |
| 114 | |
| 115 | This is necessary because when request_hooks == {'response': []}, the |
| 116 | merge breaks Session hooks entirely. |
| 117 | """ |
| 118 | if session_hooks is None or session_hooks.get("response") == []: |
| 119 | return request_hooks |
| 120 | |
| 121 | if request_hooks is None or request_hooks.get("response") == []: |
| 122 | return session_hooks |
| 123 | |
| 124 | return merge_setting(request_hooks, session_hooks, dict_class) |
| 125 | |
| 126 | |
| 127 | class SessionRedirectMixin: |
| 128 | max_redirects: int |
| 129 | trust_env: bool |
| 130 | cookies: RequestsCookieJar |
| 131 | |
| 132 | def send(self, request: PreparedRequest, **kwargs: Any) -> Response: ... |
| 133 | |
| 134 | def get_redirect_target(self, resp: Response) -> str | None: |
| 135 | """Receives a Response. Returns a redirect URI or ``None``""" |
| 136 | # Due to the nature of how requests processes redirects this method will |
| 137 | # be called at least once upon the original response and at least twice |
| 138 | # on each subsequent redirect response (if any). |
| 139 | # If a custom mixin is used to handle this logic, it may be advantageous |
| 140 | # to cache the redirect location onto the response object as a private |
| 141 | # attribute. |
| 142 | if resp.is_redirect: |
| 143 | location = resp.headers["location"] |
| 144 | # Currently the underlying http module on py3 decode headers |
| 145 | # in latin1, but empirical evidence suggests that latin1 is very |
| 146 | # rarely used with non-ASCII characters in HTTP headers. |
| 147 | # It is more likely to get UTF8 header rather than latin1. |
| 148 | # This causes incorrect handling of UTF8 encoded location headers. |
| 149 | # To solve this, we re-encode the location in latin1. |
| 150 | location = location.encode("latin1") |
| 151 | return to_native_string(location, "utf8") |
| 152 | return None |
| 153 | |
| 154 | def should_strip_auth(self, old_url: str, new_url: str) -> bool: |
| 155 | """Decide whether Authorization header should be removed when redirecting""" |
| 156 | old_parsed = urlparse(old_url) |
| 157 | new_parsed = urlparse(new_url) |
| 158 | if old_parsed.hostname != new_parsed.hostname: |
| 159 | return True |
| 160 | # Special case: allow http -> https redirect when using the standard |
| 161 | # ports. This isn't specified by RFC 7235, but is kept to avoid |
| 162 | # breaking backwards compatibility with older versions of requests |
| 163 | # that allowed any redirects on the same host. |
| 164 | if ( |
| 165 | old_parsed.scheme == "http" |
| 166 | and old_parsed.port in (80, None) |
| 167 | and new_parsed.scheme == "https" |
| 168 | and new_parsed.port in (443, None) |
| 169 | ): |
| 170 | return False |
| 171 | |
| 172 | # Handle default port usage corresponding to scheme. |
| 173 | changed_port = old_parsed.port != new_parsed.port |
| 174 | changed_scheme = old_parsed.scheme != new_parsed.scheme |
| 175 | default_port = (DEFAULT_PORTS.get(old_parsed.scheme, None), None) |
| 176 | if ( |
| 177 | not changed_scheme |
| 178 | and old_parsed.port in default_port |
| 179 | and new_parsed.port in default_port |
| 180 | ): |
| 181 | return False |
| 182 | |
| 183 | # Standard case: root URI must match |
| 184 | return changed_port or changed_scheme |
| 185 | |
| 186 | def resolve_redirects( |
| 187 | self, |
| 188 | resp: Response, |
| 189 | req: PreparedRequest, |
| 190 | stream: bool = False, |
| 191 | timeout: _t.TimeoutType = None, |
| 192 | verify: _t.VerifyType = True, |
| 193 | cert: _t.CertType = None, |
| 194 | proxies: dict[str, str] | None = None, |
| 195 | yield_requests: bool = False, |
| 196 | **adapter_kwargs: Any, |
| 197 | ) -> Generator[Response, None, None]: |
| 198 | """Receives a Response. Returns a generator of Responses or Requests.""" |
| 199 | |
| 200 | hist: list[Response] = [] # keep track of history |
| 201 | |
| 202 | url = self.get_redirect_target(resp) |
| 203 | previous_fragment = urlparse(req.url).fragment |
| 204 | while url: |
| 205 | prepared_request = req.copy() |
| 206 | |
| 207 | # Update history and keep track of redirects. |
| 208 | resp.history = hist[:] |
| 209 | hist.append(resp) |
| 210 | |
| 211 | try: |
| 212 | resp.content # Consume socket so it can be released |
| 213 | except (ChunkedEncodingError, ContentDecodingError, RuntimeError): |
| 214 | resp.raw.read(decode_content=False) |
| 215 | |
| 216 | if len(resp.history) >= self.max_redirects: |
| 217 | raise TooManyRedirects( |
| 218 | f"Exceeded {self.max_redirects} redirects.", response=resp |
| 219 | ) |
| 220 | |
| 221 | # Release the connection back into the pool. |
| 222 | resp.close() |
| 223 | |
| 224 | # Handle redirection without scheme (see: RFC 1808 Section 4) |
| 225 | if url.startswith("//"): |
| 226 | parsed_rurl = urlparse(resp.url) |
| 227 | url = ":".join([to_native_string(parsed_rurl.scheme), url]) |
| 228 | |
| 229 | # Normalize url case and attach previous fragment if needed (RFC 7231 7.1.2) |
| 230 | parsed = urlparse(url) |
| 231 | if parsed.fragment == "" and previous_fragment: |
| 232 | parsed = parsed._replace(fragment=previous_fragment) |
| 233 | elif parsed.fragment: |
| 234 | previous_fragment = parsed.fragment |
| 235 | url = parsed.geturl() |
| 236 | |
| 237 | # Facilitate relative 'location' headers, as allowed by RFC 7231. |
| 238 | # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource') |
| 239 | # Compliant with RFC3986, we percent encode the url. |
| 240 | if not parsed.netloc: |
| 241 | url = urljoin(resp.url, requote_uri(url)) |
| 242 | else: |
| 243 | url = requote_uri(url) |
| 244 | |
| 245 | prepared_request.url = to_native_string(url) |
| 246 | |
| 247 | self.rebuild_method(prepared_request, resp) |
| 248 | |
| 249 | # https://github.com/psf/requests/issues/1084 |
| 250 | if resp.status_code not in ( |
| 251 | codes.temporary_redirect, |
| 252 | codes.permanent_redirect, |
| 253 | ): |
| 254 | # https://github.com/psf/requests/issues/3490 |
| 255 | purged_headers = ("Content-Length", "Content-Type", "Transfer-Encoding") |
| 256 | for header in purged_headers: |
| 257 | prepared_request.headers.pop(header, None) |
| 258 | prepared_request.body = None |
| 259 | |
| 260 | headers = prepared_request.headers |
| 261 | headers.pop("Cookie", None) |
| 262 | |
| 263 | # Extract any cookies sent on the response to the cookiejar |
| 264 | # in the new request. Because we've mutated our copied prepared |
| 265 | # request, use the old one that we haven't yet touched. |
| 266 | cookie_jar = cast("CookieJar", prepared_request._cookies) # type: ignore[reportPrivateUsage] |
| 267 | extract_cookies_to_jar(cookie_jar, req, resp.raw) |
| 268 | merge_cookies(cookie_jar, self.cookies) |
| 269 | prepared_request.prepare_cookies(cookie_jar) |
| 270 | |
| 271 | # Rebuild auth and proxy information. |
| 272 | proxies = self.rebuild_proxies(prepared_request, proxies) |
| 273 | self.rebuild_auth(prepared_request, resp) |
| 274 | |
| 275 | # A failed tell() sets `_body_position` to `object()`. This non-None |
| 276 | # value ensures `rewindable` will be True, allowing us to raise an |
| 277 | # UnrewindableBodyError, instead of hanging the connection. |
| 278 | rewindable = prepared_request._body_position is not None and ( # type: ignore[reportPrivateUsage] |
| 279 | "Content-Length" in headers or "Transfer-Encoding" in headers |
| 280 | ) |
| 281 | |
| 282 | # Attempt to rewind consumed file-like object. |
| 283 | if rewindable: |
| 284 | rewind_body(prepared_request) |
| 285 | |
| 286 | # Override the original request. |
| 287 | req = prepared_request |
| 288 | |
| 289 | if yield_requests: |
| 290 | yield req # type: ignore[misc] # Internal use only, returns PreparedRequest |
| 291 | else: |
| 292 | resp = self.send( |
| 293 | req, |
| 294 | stream=stream, |
| 295 | timeout=timeout, |
| 296 | verify=verify, |
| 297 | cert=cert, |
| 298 | proxies=proxies, |
| 299 | allow_redirects=False, |
| 300 | **adapter_kwargs, |
| 301 | ) |
| 302 | |
| 303 | extract_cookies_to_jar(self.cookies, prepared_request, resp.raw) |
| 304 | |
| 305 | # extract redirect url, if any, for the next loop |
| 306 | url = self.get_redirect_target(resp) |
| 307 | yield resp |
| 308 | |
| 309 | def rebuild_auth( |
| 310 | self, prepared_request: PreparedRequest, response: Response |
| 311 | ) -> None: |
| 312 | """When being redirected we may want to strip authentication from the |
| 313 | request to avoid leaking credentials. This method intelligently removes |
| 314 | and reapplies authentication where possible to avoid credential loss. |
| 315 | """ |
| 316 | original_request = response.request |
| 317 | assert _is_prepared(original_request) |
| 318 | assert _is_prepared(prepared_request) |
| 319 | |
| 320 | headers = prepared_request.headers |
| 321 | original_url = original_request.url |
| 322 | url = prepared_request.url |
| 323 | |
| 324 | if "Authorization" in headers and self.should_strip_auth(original_url, url): |
| 325 | # If we get redirected to a new host, we should strip out any |
| 326 | # authentication headers. |
| 327 | del headers["Authorization"] |
| 328 | |
| 329 | # .netrc might have more auth for us on our new host. |
| 330 | new_auth = get_netrc_auth(url) if self.trust_env else None |
| 331 | if new_auth is not None: |
| 332 | prepared_request.prepare_auth(new_auth) |
| 333 | |
| 334 | def rebuild_proxies( |
| 335 | self, |
| 336 | prepared_request: PreparedRequest, |
| 337 | proxies: dict[str, str] | None, |
| 338 | ) -> dict[str, str]: |
| 339 | """This method re-evaluates the proxy configuration by considering the |
| 340 | environment variables. If we are redirected to a URL covered by |
| 341 | NO_PROXY, we strip the proxy configuration. Otherwise, we set missing |
| 342 | proxy keys for this URL (in case they were stripped by a previous |
| 343 | redirect). |
| 344 | |
| 345 | This method also replaces the Proxy-Authorization header where |
| 346 | necessary. |
| 347 | |
| 348 | :rtype: dict |
| 349 | """ |
| 350 | assert _is_prepared(prepared_request) |
| 351 | headers = prepared_request.headers |
| 352 | scheme = urlparse(prepared_request.url).scheme |
| 353 | new_proxies = resolve_proxies(prepared_request, proxies, self.trust_env) |
| 354 | |
| 355 | if "Proxy-Authorization" in headers: |
| 356 | del headers["Proxy-Authorization"] |
| 357 | |
| 358 | try: |
| 359 | username, password = get_auth_from_url(new_proxies[scheme]) |
| 360 | except KeyError: |
| 361 | username, password = None, None |
| 362 | |
| 363 | # urllib3 handles proxy authorization for us in the standard adapter. |
| 364 | # Avoid appending this to TLS tunneled requests where it may be leaked. |
| 365 | if not scheme.startswith("https") and username and password: |
| 366 | headers["Proxy-Authorization"] = _basic_auth_str(username, password) |
| 367 | |
| 368 | return new_proxies |
| 369 | |
| 370 | def rebuild_method( |
| 371 | self, prepared_request: PreparedRequest, response: Response |
| 372 | ) -> None: |
| 373 | """When being redirected we may want to change the method of the request |
| 374 | based on certain specs or browser behavior. |
| 375 | """ |
| 376 | method = prepared_request.method |
| 377 | |
| 378 | # https://tools.ietf.org/html/rfc7231#section-6.4.4 |
| 379 | if response.status_code == codes.see_other and method != "HEAD": |
| 380 | method = "GET" |
| 381 | |
| 382 | # Do what the browsers do, despite standards... |
| 383 | # First, turn 302s into GETs. |
| 384 | if response.status_code == codes.found and method != "HEAD": |
| 385 | method = "GET" |
| 386 | |
| 387 | # Second, if a POST is responded to with a 301, turn it into a GET. |
| 388 | # This bizarre behaviour is explained in Issue 1704. |
| 389 | if response.status_code == codes.moved and method == "POST": |
| 390 | method = "GET" |
| 391 | |
| 392 | prepared_request.method = method |
| 393 | |
| 394 | |
| 395 | class Session(SessionRedirectMixin): |
| 396 | """A Requests session. |
| 397 | |
| 398 | Provides cookie persistence, connection-pooling, and configuration. |
| 399 | |
| 400 | Basic Usage:: |
| 401 | |
| 402 | >>> import requests |
| 403 | >>> s = requests.Session() |
| 404 | >>> s.get('https://httpbin.org/get') |
| 405 | <Response [200]> |
| 406 | |
| 407 | Or as a context manager:: |
| 408 | |
| 409 | >>> with requests.Session() as s: |
| 410 | ... s.get('https://httpbin.org/get') |
| 411 | <Response [200]> |
| 412 | """ |
| 413 | |
| 414 | headers: CaseInsensitiveDict[str] |
| 415 | auth: _t.AuthType |
| 416 | proxies: dict[str, str] |
| 417 | hooks: dict[str, list[_t.HookType]] |
| 418 | params: MutableMapping[str, Any] |
| 419 | stream: bool |
| 420 | verify: _t.VerifyType |
| 421 | cert: _t.CertType |
| 422 | max_redirects: int |
| 423 | trust_env: bool |
| 424 | cookies: RequestsCookieJar |
| 425 | adapters: MutableMapping[str, BaseAdapter] |
| 426 | |
| 427 | __attrs__: list[str] = [ |
| 428 | "headers", |
| 429 | "cookies", |
| 430 | "auth", |
| 431 | "proxies", |
| 432 | "hooks", |
| 433 | "params", |
| 434 | "verify", |
| 435 | "cert", |
| 436 | "adapters", |
| 437 | "stream", |
| 438 | "trust_env", |
| 439 | "max_redirects", |
| 440 | ] |
| 441 | |
| 442 | def __init__(self) -> None: |
| 443 | #: A case-insensitive dictionary of headers to be sent on each |
| 444 | #: :class:`Request <Request>` sent from this |
| 445 | #: :class:`Session <Session>`. |
| 446 | self.headers = default_headers() |
| 447 | |
| 448 | #: Default Authentication tuple or object to attach to |
| 449 | #: :class:`Request <Request>`. |
| 450 | self.auth = None |
| 451 | |
| 452 | #: Dictionary mapping protocol or protocol and host to the URL of the proxy |
| 453 | #: (e.g. {'http': 'foo.bar:3128', 'http://host.name': 'foo.bar:4012'}) to |
| 454 | #: be used on each :class:`Request <Request>`. |
| 455 | self.proxies = {} |
| 456 | |
| 457 | #: Event-handling hooks. |
| 458 | self.hooks = default_hooks() |
| 459 | |
| 460 | #: Dictionary of querystring data to attach to each |
| 461 | #: :class:`Request <Request>`. The dictionary values may be lists for |
| 462 | #: representing multivalued query parameters. |
| 463 | self.params = {} |
| 464 | |
| 465 | #: Stream response content default. |
| 466 | self.stream = False |
| 467 | |
| 468 | #: SSL Verification default. |
| 469 | #: Defaults to `True`, requiring requests to verify the TLS certificate at the |
| 470 | #: remote end. |
| 471 | #: If verify is set to `False`, requests will accept any TLS certificate |
| 472 | #: presented by the server, and will ignore hostname mismatches and/or |
| 473 | #: expired certificates, which will make your application vulnerable to |
| 474 | #: man-in-the-middle (MitM) attacks. |
| 475 | #: Only set this to `False` for testing. |
| 476 | #: If verify is set to a string, it must be the path to a CA bundle file |
| 477 | #: that will be used to verify the TLS certificate. |
| 478 | self.verify = True |
| 479 | |
| 480 | #: SSL client certificate default, if String, path to ssl client |
| 481 | #: cert file (.pem). If Tuple, ('cert', 'key') pair. |
| 482 | self.cert = None |
| 483 | |
| 484 | #: Maximum number of redirects allowed. If the request exceeds this |
| 485 | #: limit, a :class:`TooManyRedirects` exception is raised. |
| 486 | #: This defaults to requests.models.DEFAULT_REDIRECT_LIMIT, which is |
| 487 | #: 30. |
| 488 | self.max_redirects = DEFAULT_REDIRECT_LIMIT |
| 489 | |
| 490 | #: Trust environment settings for proxy configuration, default |
| 491 | #: authentication and similar. |
| 492 | self.trust_env = True |
| 493 | |
| 494 | #: A CookieJar containing all currently outstanding cookies set on this |
| 495 | #: session. By default it is a |
| 496 | #: :class:`RequestsCookieJar <requests.cookies.RequestsCookieJar>`, but |
| 497 | #: may be any other ``cookielib.CookieJar`` compatible object. |
| 498 | self.cookies = cookiejar_from_dict({}) |
| 499 | |
| 500 | # Default connection adapters. |
| 501 | self.adapters = OrderedDict() |
| 502 | self.mount("https://", HTTPAdapter()) |
| 503 | self.mount("http://", HTTPAdapter()) |
| 504 | |
| 505 | def __enter__(self) -> Self: |
| 506 | return self |
| 507 | |
| 508 | def __exit__(self, *args: Any) -> None: |
| 509 | self.close() |
| 510 | |
| 511 | def prepare_request(self, request: Request) -> PreparedRequest: |
| 512 | """Constructs a :class:`PreparedRequest <PreparedRequest>` for |
| 513 | transmission and returns it. The :class:`PreparedRequest` has settings |
| 514 | merged from the :class:`Request <Request>` instance and those of the |
| 515 | :class:`Session`. |
| 516 | |
| 517 | :param request: :class:`Request` instance to prepare with this |
| 518 | session's settings. |
| 519 | :rtype: requests.PreparedRequest |
| 520 | """ |
| 521 | url = cast("_t.UriType", request.url) |
| 522 | method = cast(str, request.method) |
| 523 | |
| 524 | cookies = request.cookies or {} |
| 525 | |
| 526 | # Bootstrap CookieJar. |
| 527 | if not isinstance(cookies, cookielib.CookieJar): |
| 528 | cookies = cookiejar_from_dict(cookies) |
| 529 | |
| 530 | # Merge with session cookies |
| 531 | merged_cookies = merge_cookies( |
| 532 | merge_cookies(RequestsCookieJar(), self.cookies), cookies |
| 533 | ) |
| 534 | |
| 535 | # Set environment's basic authentication if not explicitly set. |
| 536 | auth = request.auth |
| 537 | if self.trust_env and not auth and not self.auth: |
| 538 | auth = get_netrc_auth(url) |
| 539 | |
| 540 | p = PreparedRequest() |
| 541 | p.prepare( |
| 542 | method=method.upper(), |
| 543 | url=url, |
| 544 | files=request.files, |
| 545 | data=request.data, |
| 546 | json=request.json, |
| 547 | headers=merge_setting( |
| 548 | request.headers, self.headers, dict_class=CaseInsensitiveDict |
| 549 | ), |
| 550 | params=merge_setting(request.params, self.params), |
| 551 | auth=merge_setting(auth, self.auth), |
| 552 | cookies=merged_cookies, |
| 553 | hooks=merge_hooks(request.hooks, self.hooks), |
| 554 | ) |
| 555 | return p |
| 556 | |
| 557 | def request( |
| 558 | self, |
| 559 | method: str, |
| 560 | url: _t.UriType, |
| 561 | params: _t.ParamsType = None, |
| 562 | data: _t.DataType = None, |
| 563 | headers: _t.HeadersType = None, |
| 564 | cookies: RequestsCookieJar | CookieJar | dict[str, str] | None = None, |
| 565 | files: _t.FilesType = None, |
| 566 | auth: _t.AuthType = None, |
| 567 | timeout: _t.TimeoutType = None, |
| 568 | allow_redirects: bool = True, |
| 569 | proxies: dict[str, str] | None = None, |
| 570 | hooks: _t.HooksInputType | None = None, |
| 571 | stream: bool | None = None, |
| 572 | verify: _t.VerifyType | None = None, |
| 573 | cert: _t.CertType = None, |
| 574 | json: _t.JsonType = None, |
| 575 | ) -> Response: |
| 576 | """Constructs a :class:`Request <Request>`, prepares it and sends it. |
| 577 | Returns :class:`Response <Response>` object. |
| 578 | |
| 579 | :param method: method for the new :class:`Request` object. |
| 580 | :param url: URL for the new :class:`Request` object. |
| 581 | :param params: (optional) Dictionary or bytes to be sent in the query |
| 582 | string for the :class:`Request`. |
| 583 | :param data: (optional) Dictionary, list of tuples, bytes, or file-like |
| 584 | object to send in the body of the :class:`Request`. |
| 585 | :param json: (optional) json to send in the body of the |
| 586 | :class:`Request`. |
| 587 | :param headers: (optional) Dictionary of HTTP Headers to send with the |
| 588 | :class:`Request`. |
| 589 | :param cookies: (optional) Dict or CookieJar object to send with the |
| 590 | :class:`Request`. |
| 591 | :param files: (optional) Dictionary of ``'filename': file-like-objects`` |
| 592 | for multipart encoding upload. |
| 593 | :param auth: (optional) Auth tuple or callable to enable |
| 594 | Basic/Digest/Custom HTTP Auth. |
| 595 | :param timeout: (optional) How many seconds to wait for the server to send |
| 596 | data before giving up, as a float, or a :ref:`(connect timeout, |
| 597 | read timeout) <timeouts>` tuple. |
| 598 | :type timeout: float or tuple |
| 599 | :param allow_redirects: (optional) Set to True by default. |
| 600 | :type allow_redirects: bool |
| 601 | :param proxies: (optional) Dictionary mapping protocol or protocol and |
| 602 | hostname to the URL of the proxy. |
| 603 | :param hooks: (optional) Dictionary mapping hook name to one event or |
| 604 | list of events, event must be callable. |
| 605 | :param stream: (optional) whether to immediately download the response |
| 606 | content. Defaults to ``False``. |
| 607 | :param verify: (optional) Either a boolean, in which case it controls whether we verify |
| 608 | the server's TLS certificate, or a string, in which case it must be a path |
| 609 | to a CA bundle to use. Defaults to ``True``. When set to |
| 610 | ``False``, requests will accept any TLS certificate presented by |
| 611 | the server, and will ignore hostname mismatches and/or expired |
| 612 | certificates, which will make your application vulnerable to |
| 613 | man-in-the-middle (MitM) attacks. Setting verify to ``False`` |
| 614 | may be useful during local development or testing. |
| 615 | :param cert: (optional) if String, path to ssl client cert file (.pem). |
| 616 | If Tuple, ('cert', 'key') pair. |
| 617 | :rtype: requests.Response |
| 618 | """ |
| 619 | if isinstance(url, bytes): |
| 620 | url = url.decode("utf-8") |
| 621 | |
| 622 | # Create the Request. |
| 623 | req = Request( |
| 624 | method=method.upper(), |
| 625 | url=url, |
| 626 | headers=headers, |
| 627 | files=files, |
| 628 | data=data or {}, |
| 629 | json=json, |
| 630 | params=params or {}, |
| 631 | auth=auth, |
| 632 | cookies=cookies, |
| 633 | hooks=hooks, |
| 634 | ) |
| 635 | prep = self.prepare_request(req) |
| 636 | |
| 637 | assert _is_prepared(prep) |
| 638 | |
| 639 | proxies = proxies or {} |
| 640 | |
| 641 | settings = self.merge_environment_settings( |
| 642 | prep.url, proxies, stream, verify, cert |
| 643 | ) |
| 644 | |
| 645 | # Send the request. |
| 646 | send_kwargs = { |
| 647 | "timeout": timeout, |
| 648 | "allow_redirects": allow_redirects, |
| 649 | } |
| 650 | send_kwargs.update(settings) |
| 651 | resp = self.send(prep, **send_kwargs) |
| 652 | |
| 653 | return resp |
| 654 | |
| 655 | def get( |
| 656 | self, |
| 657 | url: _t.UriType, |
| 658 | params: _t.ParamsType = None, |
| 659 | **kwargs: Unpack[_t.GetKwargs], |
| 660 | ) -> Response: |
| 661 | r"""Sends a GET request. Returns :class:`Response` object. |
| 662 | |
| 663 | :param url: URL for the new :class:`Request` object. |
| 664 | :param params: (optional) Dictionary, list of tuples or bytes to send |
| 665 | in the query string for the :class:`Request`. |
| 666 | :param \*\*kwargs: Optional arguments that ``request`` takes. |
| 667 | :rtype: requests.Response |
| 668 | """ |
| 669 | |
| 670 | kwargs.setdefault("allow_redirects", True) |
| 671 | return self.request("GET", url, params=params, **kwargs) |
| 672 | |
| 673 | def options(self, url: _t.UriType, **kwargs: Unpack[_t.RequestKwargs]) -> Response: |
| 674 | r"""Sends a OPTIONS request. Returns :class:`Response` object. |
| 675 | |
| 676 | :param url: URL for the new :class:`Request` object. |
| 677 | :param \*\*kwargs: Optional arguments that ``request`` takes. |
| 678 | :rtype: requests.Response |
| 679 | """ |
| 680 | |
| 681 | kwargs.setdefault("allow_redirects", True) |
| 682 | return self.request("OPTIONS", url, **kwargs) |
| 683 | |
| 684 | def head(self, url: _t.UriType, **kwargs: Unpack[_t.RequestKwargs]) -> Response: |
| 685 | r"""Sends a HEAD request. Returns :class:`Response` object. |
| 686 | |
| 687 | :param url: URL for the new :class:`Request` object. |
| 688 | :param \*\*kwargs: Optional arguments that ``request`` takes. |
| 689 | :rtype: requests.Response |
| 690 | """ |
| 691 | |
| 692 | kwargs.setdefault("allow_redirects", False) |
| 693 | return self.request("HEAD", url, **kwargs) |
| 694 | |
| 695 | def post( |
| 696 | self, |
| 697 | url: _t.UriType, |
| 698 | data: _t.DataType = None, |
| 699 | json: _t.JsonType = None, |
| 700 | **kwargs: Unpack[_t.PostKwargs], |
| 701 | ) -> Response: |
| 702 | r"""Sends a POST request. Returns :class:`Response` object. |
| 703 | |
| 704 | :param url: URL for the new :class:`Request` object. |
| 705 | :param data: (optional) Dictionary, list of tuples, bytes, or file-like |
| 706 | object to send in the body of the :class:`Request`. |
| 707 | :param json: (optional) json to send in the body of the :class:`Request`. |
| 708 | :param \*\*kwargs: Optional arguments that ``request`` takes. |
| 709 | :rtype: requests.Response |
| 710 | """ |
| 711 | |
| 712 | return self.request("POST", url, data=data, json=json, **kwargs) |
| 713 | |
| 714 | def put( |
| 715 | self, url: _t.UriType, data: _t.DataType = None, **kwargs: Unpack[_t.DataKwargs] |
| 716 | ) -> Response: |
| 717 | r"""Sends a PUT request. Returns :class:`Response` object. |
| 718 | |
| 719 | :param url: URL for the new :class:`Request` object. |
| 720 | :param data: (optional) Dictionary, list of tuples, bytes, or file-like |
| 721 | object to send in the body of the :class:`Request`. |
| 722 | :param \*\*kwargs: Optional arguments that ``request`` takes. |
| 723 | :rtype: requests.Response |
| 724 | """ |
| 725 | |
| 726 | return self.request("PUT", url, data=data, **kwargs) |
| 727 | |
| 728 | def patch( |
| 729 | self, url: _t.UriType, data: _t.DataType = None, **kwargs: Unpack[_t.DataKwargs] |
| 730 | ) -> Response: |
| 731 | r"""Sends a PATCH request. Returns :class:`Response` object. |
| 732 | |
| 733 | :param url: URL for the new :class:`Request` object. |
| 734 | :param data: (optional) Dictionary, list of tuples, bytes, or file-like |
| 735 | object to send in the body of the :class:`Request`. |
| 736 | :param \*\*kwargs: Optional arguments that ``request`` takes. |
| 737 | :rtype: requests.Response |
| 738 | """ |
| 739 | |
| 740 | return self.request("PATCH", url, data=data, **kwargs) |
| 741 | |
| 742 | def delete(self, url: _t.UriType, **kwargs: Unpack[_t.RequestKwargs]) -> Response: |
| 743 | r"""Sends a DELETE request. Returns :class:`Response` object. |
| 744 | |
| 745 | :param url: URL for the new :class:`Request` object. |
| 746 | :param \*\*kwargs: Optional arguments that ``request`` takes. |
| 747 | :rtype: requests.Response |
| 748 | """ |
| 749 | |
| 750 | return self.request("DELETE", url, **kwargs) |
| 751 | |
| 752 | def send(self, request: PreparedRequest, **kwargs: Any) -> Response: |
| 753 | """Send a given PreparedRequest. |
| 754 | |
| 755 | :rtype: requests.Response |
| 756 | """ |
| 757 | # Set defaults that the hooks can utilize to ensure they always have |
| 758 | # the correct parameters to reproduce the previous request. |
| 759 | kwargs.setdefault("stream", self.stream) |
| 760 | kwargs.setdefault("verify", self.verify) |
| 761 | kwargs.setdefault("cert", self.cert) |
| 762 | if "proxies" not in kwargs: |
| 763 | kwargs["proxies"] = resolve_proxies(request, self.proxies, self.trust_env) |
| 764 | |
| 765 | # It's possible that users might accidentally send a Request object. |
| 766 | # Guard against that specific failure case. |
| 767 | if isinstance(request, Request): |
| 768 | raise ValueError("You can only send PreparedRequests.") |
| 769 | |
| 770 | assert _is_prepared(request) |
| 771 | |
| 772 | # Set up variables needed for resolve_redirects and dispatching of hooks |
| 773 | allow_redirects = kwargs.pop("allow_redirects", True) |
| 774 | stream = kwargs.get("stream") |
| 775 | hooks = request.hooks |
| 776 | |
| 777 | # Get the appropriate adapter to use |
| 778 | adapter = self.get_adapter(url=request.url) |
| 779 | |
| 780 | # Start time (approximately) of the request |
| 781 | start = preferred_clock() |
| 782 | |
| 783 | # Send the request |
| 784 | r = adapter.send(request, **kwargs) |
| 785 | |
| 786 | # Total elapsed time of the request (approximately) |
| 787 | elapsed = preferred_clock() - start |
| 788 | r.elapsed = timedelta(seconds=elapsed) |
| 789 | |
| 790 | # Response manipulation hooks |
| 791 | r = dispatch_hook("response", hooks, r, **kwargs) |
| 792 | |
| 793 | # Persist cookies |
| 794 | if r.history: |
| 795 | # If the hooks create history then we want those cookies too |
| 796 | for resp in r.history: |
| 797 | extract_cookies_to_jar(self.cookies, resp.request, resp.raw) |
| 798 | |
| 799 | extract_cookies_to_jar(self.cookies, request, r.raw) |
| 800 | |
| 801 | # Resolve redirects if allowed. |
| 802 | if allow_redirects: |
| 803 | # Redirect resolving generator. |
| 804 | gen = self.resolve_redirects(r, request, **kwargs) |
| 805 | history = [resp for resp in gen] |
| 806 | else: |
| 807 | history = [] |
| 808 | |
| 809 | # Shuffle things around if there's history. |
| 810 | if history: |
| 811 | # Insert the first (original) request at the start |
| 812 | history.insert(0, r) |
| 813 | # Get the last request made |
| 814 | r = history.pop() |
| 815 | r.history = history |
| 816 | |
| 817 | # If redirects aren't being followed, store the response on the Request for Response.next(). |
| 818 | if not allow_redirects: |
| 819 | try: |
| 820 | r._next = next( # type: ignore[assignment] # yield_requests=True returns PreparedRequest |
| 821 | self.resolve_redirects(r, request, yield_requests=True, **kwargs) |
| 822 | ) |
| 823 | except StopIteration: |
| 824 | pass |
| 825 | |
| 826 | if not stream: |
| 827 | r.content |
| 828 | |
| 829 | return r |
| 830 | |
| 831 | def merge_environment_settings( |
| 832 | self, |
| 833 | url: str, |
| 834 | proxies: dict[str, str] | None, |
| 835 | stream: bool | None, |
| 836 | verify: _t.VerifyType | None, |
| 837 | cert: _t.CertType, |
| 838 | ) -> dict[str, Any]: |
| 839 | """ |
| 840 | Check the environment and merge it with some settings. |
| 841 | |
| 842 | :rtype: dict |
| 843 | """ |
| 844 | # Gather clues from the surrounding environment. |
| 845 | if self.trust_env: |
| 846 | # Set environment's proxies. |
| 847 | no_proxy = proxies.get("no_proxy") if proxies is not None else None |
| 848 | env_proxies = get_environ_proxies(url, no_proxy=no_proxy) |
| 849 | if proxies is not None: |
| 850 | for k, v in env_proxies.items(): |
| 851 | proxies.setdefault(k, v) |
| 852 | |
| 853 | # Look for requests environment configuration |
| 854 | # and be compatible with cURL. |
| 855 | if verify is True or verify is None: |
| 856 | verify = ( |
| 857 | os.environ.get("REQUESTS_CA_BUNDLE") |
| 858 | or os.environ.get("CURL_CA_BUNDLE") |
| 859 | or verify |
| 860 | ) |
| 861 | |
| 862 | # Merge all the kwargs. |
| 863 | proxies = merge_setting(proxies, self.proxies) |
| 864 | stream = merge_setting(stream, self.stream) |
| 865 | verify = merge_setting(verify, self.verify) |
| 866 | cert = merge_setting(cert, self.cert) |
| 867 | |
| 868 | return {"proxies": proxies, "stream": stream, "verify": verify, "cert": cert} |
| 869 | |
| 870 | def get_adapter(self, url: str) -> BaseAdapter: |
| 871 | """ |
| 872 | Returns the appropriate connection adapter for the given URL. |
| 873 | |
| 874 | :rtype: requests.adapters.BaseAdapter |
| 875 | """ |
| 876 | for prefix, adapter in self.adapters.items(): |
| 877 | if url.lower().startswith(prefix.lower()): |
| 878 | return adapter |
| 879 | |
| 880 | # Nothing matches :-/ |
| 881 | raise InvalidSchema(f"No connection adapters were found for {url!r}") |
| 882 | |
| 883 | def close(self) -> None: |
| 884 | """Closes all adapters and as such the session""" |
| 885 | for v in self.adapters.values(): |
| 886 | v.close() |
| 887 | |
| 888 | def mount(self, prefix: str, adapter: BaseAdapter) -> None: |
| 889 | """Registers a connection adapter to a prefix. |
| 890 | |
| 891 | Adapters are sorted in descending order by prefix length. |
| 892 | """ |
| 893 | self.adapters[prefix] = adapter |
| 894 | keys_to_move = [k for k in self.adapters if len(k) < len(prefix)] |
| 895 | |
| 896 | for key in keys_to_move: |
| 897 | self.adapters[key] = self.adapters.pop(key) |
| 898 | |
| 899 | def __getstate__(self) -> dict[str, Any]: |
| 900 | state = {attr: getattr(self, attr, None) for attr in self.__attrs__} |
| 901 | return state |
| 902 | |
| 903 | def __setstate__(self, state: dict[str, Any]) -> None: |
| 904 | for attr, value in state.items(): |
| 905 | setattr(self, attr, value) |
| 906 | |
| 907 | |
| 908 | def session() -> Session: |
| 909 | """ |
| 910 | Returns a :class:`Session` for context-management. |
| 911 | |
| 912 | .. deprecated:: 1.0.0 |
| 913 | |
| 914 | This method has been deprecated since version 1.0.0 and is only kept for |
| 915 | backwards compatibility. New code should use :class:`~requests.sessions.Session` |
| 916 | to create a session. This may be removed at a future date. |
| 917 | |
| 918 | :rtype: Session |
| 919 | """ |
| 920 | return Session() |
| 921 |