codekingpro/portable-devtools
114k
1"""2requests.cookies3~~~~~~~~~~~~~~~~4 5Compatibility code to be able to use `http.cookiejar.CookieJar` with requests.6 7requests.utils imports from here, so be careful with imports.8"""9 10from __future__ import annotations11 12import calendar13import copy14import time15from collections.abc import Iterator, MutableMapping16from http.cookiejar import Cookie, CookieJar, CookiePolicy17from typing import TYPE_CHECKING, Any, TypeVar, overload18 19from ._internal_utils import to_native_string20from ._types import is_prepared as _is_prepared21from .compat import Morsel, cookielib, urlparse, urlunparse22 23if TYPE_CHECKING:24 from _typeshed import SupportsKeysAndGetItem25 26 from .models import PreparedRequest27 28import threading29 30 31class MockRequest:32 """Wraps a `requests.PreparedRequest` to mimic a `urllib2.Request`.33 34 The code in `http.cookiejar.CookieJar` expects this interface in order to correctly35 manage cookie policies, i.e., determine whether a cookie can be set, given the36 domains of the request and the cookie.37 38 The original request object is read-only. The client is responsible for collecting39 the new headers via `get_new_headers()` and interpreting them appropriately. You40 probably want `get_cookie_header`, defined below.41 """42 43 type: str44 45 def __init__(self, request: PreparedRequest) -> None:46 assert _is_prepared(request)47 self._r = request48 self._new_headers: dict[str, str] = {}49 self.type = urlparse(self._r.url).scheme50 51 def get_type(self) -> str:52 return self.type53 54 def get_host(self) -> str:55 return urlparse(self._r.url).netloc56 57 def get_origin_req_host(self) -> str:58 return self.get_host()59 60 def get_full_url(self) -> str:61 # Only return the response's URL if the user hadn't set the Host62 # header63 if not self._r.headers.get("Host"):64 return self._r.url65 # If they did set it, retrieve it and reconstruct the expected domain66 host = to_native_string(self._r.headers["Host"], encoding="utf-8")67 parsed = urlparse(self._r.url)68 # Reconstruct the URL as we expect it69 return urlunparse(70 [71 parsed.scheme,72 host,73 parsed.path,74 parsed.params,75 parsed.query,76 parsed.fragment,77 ]78 )79 80 def is_unverifiable(self) -> bool:81 return True82 83 def has_header(self, name: str) -> bool:84 return name in self._r.headers or name in self._new_headers85 86 def get_header(self, name: str, default: str | None = None) -> str | None:87 return self._r.headers.get(name, self._new_headers.get(name, default)) # type: ignore[return-value]88 89 def add_header(self, key: str, val: str) -> None:90 """cookiejar has no legitimate use for this method; add it back if you find one."""91 raise NotImplementedError(92 "Cookie headers should be added with add_unredirected_header()"93 )94 95 def add_unredirected_header(self, name: str, value: str) -> None:96 self._new_headers[name] = value97 98 def get_new_headers(self) -> dict[str, str]:99 return self._new_headers100 101 @property102 def unverifiable(self) -> bool:103 return self.is_unverifiable()104 105 @property106 def origin_req_host(self) -> str:107 return self.get_origin_req_host()108 109 @property110 def host(self) -> str:111 return self.get_host()112 113 114class MockResponse:115 """Wraps a `httplib.HTTPMessage` to mimic a `urllib.addinfourl`.116 117 ...what? Basically, expose the parsed HTTP headers from the server response118 the way `http.cookiejar` expects to see them.119 """120 121 def __init__(self, headers: Any) -> None:122 """Make a MockResponse for `cookiejar` to read.123 124 :param headers: a httplib.HTTPMessage or analogous carrying the headers125 """126 self._headers = headers127 128 def info(self) -> Any:129 return self._headers130 131 def getheaders(self, name: str) -> Any:132 self._headers.getheaders(name)133 134 135def extract_cookies_to_jar(136 jar: CookieJar, request: PreparedRequest, response: Any137) -> None:138 """Extract the cookies from the response into a CookieJar.139 140 :param jar: http.cookiejar.CookieJar (not necessarily a RequestsCookieJar)141 :param request: our own requests.Request object142 :param response: urllib3.HTTPResponse object143 """144 if not (hasattr(response, "_original_response") and response._original_response):145 return146 # the _original_response field is the wrapped httplib.HTTPResponse object,147 req = MockRequest(request)148 # pull out the HTTPMessage with the headers and put it in the mock:149 res = MockResponse(response._original_response.msg)150 jar.extract_cookies(res, req) # type: ignore[arg-type]151 152 153def get_cookie_header(jar: CookieJar, request: PreparedRequest) -> str | None:154 """155 Produce an appropriate Cookie header string to be sent with `request`, or None.156 157 :rtype: str158 """159 r = MockRequest(request)160 jar.add_cookie_header(r) # type: ignore[arg-type]161 return r.get_new_headers().get("Cookie")162 163 164def remove_cookie_by_name(165 cookiejar: CookieJar, name: str, domain: str | None = None, path: str | None = None166) -> None:167 """Unsets a cookie by name, by default over all domains and paths.168 169 Wraps CookieJar.clear(), is O(n).170 """171 clearables: list[tuple[str, str, str]] = []172 for cookie in cookiejar:173 if cookie.name != name:174 continue175 if domain is not None and domain != cookie.domain:176 continue177 if path is not None and path != cookie.path:178 continue179 clearables.append((cookie.domain, cookie.path, cookie.name))180 181 for domain, path, name in clearables:182 cookiejar.clear(domain, path, name)183 184 185class CookieConflictError(RuntimeError):186 """There are two cookies that meet the criteria specified in the cookie jar.187 Use .get and .set and include domain and path args in order to be more specific.188 """189 190 191class RequestsCookieJar(CookieJar, MutableMapping[str, str | None]): # type: ignore[misc]192 """Compatibility class; is a http.cookiejar.CookieJar, but exposes a dict193 interface.194 195 This is the CookieJar we create by default for requests and sessions that196 don't specify one, since some clients may expect response.cookies and197 session.cookies to support dict operations.198 199 Requests does not use the dict interface internally; it's just for200 compatibility with external client code. All requests code should work201 out of the box with externally provided instances of ``CookieJar``, e.g.202 ``LWPCookieJar`` and ``FileCookieJar``.203 204 Unlike a regular CookieJar, this class is pickleable.205 206 .. warning:: dictionary operations that are normally O(1) may be O(n).207 """208 209 _policy: CookiePolicy210 211 def get( # type: ignore[override]212 self,213 name: str,214 default: str | None = None,215 domain: str | None = None,216 path: str | None = None,217 ) -> str | None:218 """Dict-like get() that also supports optional domain and path args in219 order to resolve naming collisions from using one cookie jar over220 multiple domains.221 222 .. warning:: operation is O(n), not O(1).223 """224 try:225 return self._find_no_duplicates(name, domain, path)226 except KeyError:227 return default228 229 def set(230 self, name: str, value: str | Morsel[dict[str, str]] | None, **kwargs: Any231 ) -> Cookie | None:232 """Dict-like set() that also supports optional domain and path args in233 order to resolve naming collisions from using one cookie jar over234 multiple domains.235 """236 # support client code that unsets cookies by assignment of a None value:237 if value is None:238 remove_cookie_by_name(239 self, name, domain=kwargs.get("domain"), path=kwargs.get("path")240 )241 return242 243 if isinstance(value, Morsel):244 c = morsel_to_cookie(value)245 else:246 c = create_cookie(name, value, **kwargs)247 self.set_cookie(c)248 return c249 250 def iterkeys(self) -> Iterator[str]:251 """Dict-like iterkeys() that returns an iterator of names of cookies252 from the jar.253 254 .. seealso:: itervalues() and iteritems().255 """256 for cookie in iter(self):257 yield cookie.name258 259 def keys(self) -> list[str]: # type: ignore[override]260 """Dict-like keys() that returns a list of names of cookies from the261 jar.262 263 .. seealso:: values() and items().264 """265 return list(self.iterkeys())266 267 def itervalues(self) -> Iterator[str | None]:268 """Dict-like itervalues() that returns an iterator of values of cookies269 from the jar.270 271 .. seealso:: iterkeys() and iteritems().272 """273 for cookie in iter(self):274 yield cookie.value275 276 def values(self) -> list[str | None]: # type: ignore[override]277 """Dict-like values() that returns a list of values of cookies from the278 jar.279 280 .. seealso:: keys() and items().281 """282 return list(self.itervalues())283 284 def iteritems(self) -> Iterator[tuple[str, str | None]]:285 """Dict-like iteritems() that returns an iterator of name-value tuples286 from the jar.287 288 .. seealso:: iterkeys() and itervalues().289 """290 for cookie in iter(self):291 yield cookie.name, cookie.value292 293 def items(self) -> list[tuple[str, str | None]]: # type: ignore[override]294 """Dict-like items() that returns a list of name-value tuples from the295 jar. Allows client-code to call ``dict(RequestsCookieJar)`` and get a296 vanilla python dict of key value pairs.297 298 .. seealso:: keys() and values().299 """300 return list(self.iteritems())301 302 def list_domains(self) -> list[str]:303 """Utility method to list all the domains in the jar."""304 domains: list[str] = []305 for cookie in iter(self):306 if cookie.domain not in domains:307 domains.append(cookie.domain)308 return domains309 310 def list_paths(self) -> list[str]:311 """Utility method to list all the paths in the jar."""312 paths: list[str] = []313 for cookie in iter(self):314 if cookie.path not in paths:315 paths.append(cookie.path)316 return paths317 318 def multiple_domains(self) -> bool:319 """Returns True if there are multiple domains in the jar.320 Returns False otherwise.321 322 :rtype: bool323 """324 domains: list[str] = []325 for cookie in iter(self):326 if cookie.domain is not None and cookie.domain in domains: # type: ignore[reportUnnecessaryComparison] # defensive check327 return True328 domains.append(cookie.domain)329 return False # there is only one domain in jar330 331 def get_dict(332 self, domain: str | None = None, path: str | None = None333 ) -> dict[str, str | None]:334 """Takes as an argument an optional domain and path and returns a plain335 old Python dict of name-value pairs of cookies that meet the336 requirements.337 338 :rtype: dict339 """340 dictionary: dict[str, str | None] = {}341 for cookie in iter(self):342 if (domain is None or cookie.domain == domain) and (343 path is None or cookie.path == path344 ):345 dictionary[cookie.name] = cookie.value346 return dictionary347 348 def __iter__(self) -> Iterator[Cookie]: # type: ignore[override]349 """RequestCookieJar's __iter__ comes from CookieJar not MutableMapping."""350 return super().__iter__()351 352 def __contains__(self, name: object) -> bool:353 try:354 return super().__contains__(name)355 except CookieConflictError:356 return True357 358 def __getitem__(self, name: str) -> str | None:359 """Dict-like __getitem__() for compatibility with client code. Throws360 exception if there are more than one cookie with name. In that case,361 use the more explicit get() method instead.362 363 .. warning:: operation is O(n), not O(1).364 """365 return self._find_no_duplicates(name)366 367 def __setitem__(368 self, name: str, value: str | Morsel[dict[str, str]] | None369 ) -> None:370 """Dict-like __setitem__ for compatibility with client code. Throws371 exception if there is already a cookie of that name in the jar. In that372 case, use the more explicit set() method instead.373 """374 self.set(name, value)375 376 def __delitem__(self, name: str) -> None:377 """Deletes a cookie given a name. Wraps ``http.cookiejar.CookieJar``'s378 ``remove_cookie_by_name()``.379 """380 remove_cookie_by_name(self, name)381 382 def set_cookie(self, cookie: Cookie, *args: Any, **kwargs: Any) -> None:383 if (384 (value := cookie.value) is not None385 and value.startswith('"')386 and value.endswith('"')387 ):388 cookie.value = value.replace('\\"', "")389 return super().set_cookie(cookie, *args, **kwargs)390 391 def update( # type: ignore[override]392 self, other: CookieJar | SupportsKeysAndGetItem[str, str]393 ) -> None:394 """Updates this jar with cookies from another CookieJar or dict-like"""395 if isinstance(other, cookielib.CookieJar):396 for cookie in other:397 self.set_cookie(copy.copy(cookie))398 else:399 super().update(other)400 401 def _find(402 self, name: str, domain: str | None = None, path: str | None = None403 ) -> str | None:404 """Requests uses this method internally to get cookie values.405 406 If there are conflicting cookies, _find arbitrarily chooses one.407 See _find_no_duplicates if you want an exception thrown if there are408 conflicting cookies.409 410 :param name: a string containing name of cookie411 :param domain: (optional) string containing domain of cookie412 :param path: (optional) string containing path of cookie413 :return: cookie.value414 """415 for cookie in iter(self):416 if cookie.name == name:417 if domain is None or cookie.domain == domain:418 if path is None or cookie.path == path:419 return cookie.value420 421 raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}")422 423 def _find_no_duplicates(424 self, name: str, domain: str | None = None, path: str | None = None425 ) -> str:426 """Both ``__get_item__`` and ``get`` call this function: it's never427 used elsewhere in Requests.428 429 :param name: a string containing name of cookie430 :param domain: (optional) string containing domain of cookie431 :param path: (optional) string containing path of cookie432 :raises KeyError: if cookie is not found433 :raises CookieConflictError: if there are multiple cookies434 that match name and optionally domain and path435 :return: cookie.value436 """437 toReturn = None438 for cookie in iter(self):439 if cookie.name == name:440 if domain is None or cookie.domain == domain:441 if path is None or cookie.path == path:442 if toReturn is not None:443 # if there are multiple cookies that meet passed in criteria444 raise CookieConflictError(445 f"There are multiple cookies with name, {name!r}"446 )447 # we will eventually return this as long as no cookie conflict448 toReturn = cookie.value449 450 if toReturn is not None:451 return toReturn452 raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}")453 454 def __getstate__(self) -> dict[str, Any]:455 """Unlike a normal CookieJar, this class is pickleable."""456 state = self.__dict__.copy()457 # remove the unpickleable RLock object458 state.pop("_cookies_lock")459 return state460 461 def __setstate__(self, state: dict[str, Any]) -> None:462 """Unlike a normal CookieJar, this class is pickleable."""463 self.__dict__.update(state)464 if "_cookies_lock" not in self.__dict__:465 self._cookies_lock = threading.RLock()466 467 def copy(self) -> RequestsCookieJar:468 """Return a copy of this RequestsCookieJar."""469 new_cj = RequestsCookieJar()470 new_cj.set_policy(self.get_policy())471 new_cj.update(self)472 return new_cj473 474 def get_policy(self) -> CookiePolicy:475 """Return the CookiePolicy instance used."""476 return self._policy477 478 479def _copy_cookie_jar(jar: CookieJar | None) -> CookieJar | None: # type: ignore[reportUnusedFunction] # cross-module usage in models.py480 if jar is None:481 return None482 483 if copy_method := getattr(jar, "copy", None):484 # We're dealing with an instance of RequestsCookieJar485 return copy_method()486 # We're dealing with a generic CookieJar instance487 new_jar = copy.copy(jar)488 new_jar.clear()489 for cookie in jar:490 new_jar.set_cookie(copy.copy(cookie))491 return new_jar492 493 494def create_cookie(name: str, value: str, **kwargs: Any) -> Cookie:495 """Make a cookie from underspecified parameters.496 497 By default, the pair of `name` and `value` will be set for the domain ''498 and sent on every request (this is sometimes called a "supercookie").499 """500 result: dict[str, Any] = {501 "version": 0,502 "name": name,503 "value": value,504 "port": None,505 "domain": "",506 "path": "/",507 "secure": False,508 "expires": None,509 "discard": True,510 "comment": None,511 "comment_url": None,512 "rest": {"HttpOnly": None},513 "rfc2109": False,514 }515 516 badargs = set(kwargs) - set(result)517 if badargs:518 raise TypeError(519 f"create_cookie() got unexpected keyword arguments: {list(badargs)}"520 )521 522 result.update(kwargs)523 result["port_specified"] = bool(result["port"])524 result["domain_specified"] = bool(result["domain"])525 result["domain_initial_dot"] = result["domain"].startswith(".")526 result["path_specified"] = bool(result["path"])527 528 return cookielib.Cookie(**result)529 530 531def morsel_to_cookie(morsel: Morsel[Any]) -> Cookie:532 """Convert a Morsel object into a Cookie containing the one k/v pair."""533 534 expires: int | None = None535 if morsel["max-age"]:536 try:537 expires = int(time.time() + int(morsel["max-age"]))538 except ValueError:539 raise TypeError(f"max-age: {morsel['max-age']} must be integer")540 elif morsel["expires"]:541 time_template = "%a, %d-%b-%Y %H:%M:%S GMT"542 expires = calendar.timegm(time.strptime(morsel["expires"], time_template))543 return create_cookie(544 comment=morsel["comment"],545 comment_url=bool(morsel["comment"]),546 discard=False,547 domain=morsel["domain"],548 expires=expires,549 name=morsel.key,550 path=morsel["path"],551 port=None,552 rest={"HttpOnly": morsel["httponly"]},553 rfc2109=False,554 secure=bool(morsel["secure"]),555 value=morsel.value,556 version=morsel["version"] or 0,557 )558 559 560_CookieJarT = TypeVar("_CookieJarT", bound=CookieJar)561 562 563@overload564def cookiejar_from_dict(565 cookie_dict: dict[str, str] | None,566 cookiejar: None = None,567 overwrite: bool = True,568) -> RequestsCookieJar: ...569 570 571@overload572def cookiejar_from_dict(573 cookie_dict: dict[str, str] | None,574 cookiejar: _CookieJarT,575 overwrite: bool = True,576) -> _CookieJarT: ...577 578 579def cookiejar_from_dict(580 cookie_dict: dict[str, str] | None,581 cookiejar: CookieJar | None = None,582 overwrite: bool = True,583) -> CookieJar:584 """Returns a CookieJar from a key/value dictionary.585 586 :param cookie_dict: Dict of key/values to insert into CookieJar.587 :param cookiejar: (optional) A cookiejar to add the cookies to.588 :param overwrite: (optional) If False, will not replace cookies589 already in the jar with new ones.590 :rtype: CookieJar591 """592 if cookiejar is None:593 cookiejar = RequestsCookieJar()594 595 if cookie_dict is not None:596 names_from_jar = [cookie.name for cookie in cookiejar]597 for name in cookie_dict:598 if overwrite or (name not in names_from_jar):599 cookiejar.set_cookie(create_cookie(name, cookie_dict[name]))600 601 return cookiejar602 603 604def merge_cookies(605 cookiejar: CookieJar, cookies: dict[str, str] | CookieJar | None606) -> CookieJar:607 """Add cookies to cookiejar and returns a merged CookieJar.608 609 :param cookiejar: CookieJar object to add the cookies to.610 :param cookies: Dictionary or CookieJar object to be added.611 :rtype: CookieJar612 """613 if not isinstance(cookiejar, cookielib.CookieJar): # type: ignore[reportUnnecessaryIsInstance] # runtime guard614 raise ValueError("You can only merge into CookieJar")615 616 if isinstance(cookies, dict):617 cookiejar = cookiejar_from_dict(cookies, cookiejar=cookiejar, overwrite=False)618 elif isinstance(cookies, cookielib.CookieJar):619 if update_method := getattr(cookiejar, "update", None):620 update_method(cookies)621 else:622 for cookie_in_jar in cookies:623 cookiejar.set_cookie(cookie_in_jar)624 625 return cookiejar626 