# Copied from: https://github.com/encode/httpx/blob/master/httpx/_models.py, # which is licensed under the BSD License. # See https://github.com/encode/httpx/blob/master/LICENSE.md import email.message import typing import urllib.request import warnings from http.cookiejar import Cookie, CookieJar from json import loads from .. import Curl from .errors import RequestsError from .headers import Headers CookieTypes = typing.Union[ "Cookies", CookieJar, typing.Dict[str, str], typing.List[typing.Tuple[str, str]] ] class Request: def __init__(self, url: str, headers: Headers, method: str): self.url = url self.headers = headers self.method = method class Response: def __init__(self, curl: Curl, request: Request): self.curl = curl self.request = request self.url = "" self.content = b"" self.status_code = 200 self.reason = "OK" self.ok = True self.headers = Headers() self.cookies = Cookies() self.elapsed = 0.0 self.encoding = "utf-8" self.charset = self.encoding self.redirect_count = 0 self.redirect_url = "" @property def text(self) -> str: return self.content.decode(self.charset) def raise_for_status(self): if not self.ok: raise RequestsError(f"HTTP Error {self.status_code}: {self.reason}") def json(self, **kw): return loads(self.content, **kw) def close(self): warnings.warn("Deprecated, use Session.close") class Cookies(typing.MutableMapping[str, str]): """ HTTP Cookies, as a mutable mapping. """ def __init__(self, cookies: typing.Optional[CookieTypes] = None) -> None: if cookies is None or isinstance(cookies, dict): self.jar = CookieJar() if isinstance(cookies, dict): for key, value in cookies.items(): self.set(key, value) elif isinstance(cookies, list): self.jar = CookieJar() for key, value in cookies: self.set(key, value) elif isinstance(cookies, Cookies): self.jar = CookieJar() for cookie in cookies.jar: self.jar.set_cookie(cookie) else: self.jar = cookies def extract_cookies(self, response: Response) -> None: """ Loads any cookies based on the response `Set-Cookie` headers. """ urllib_response = self._CookieCompatResponse(response) urllib_request = self._CookieCompatRequest(response.request) # print("cookies extracted: ", self.jar.make_cookies(urllib_response, urllib_request)) self.jar.extract_cookies(urllib_response, urllib_request) # type: ignore def set_cookie_header(self, request: Request) -> None: """ Sets an appropriate 'Cookie:' HTTP header on the `Request`. """ urllib_request = self._CookieCompatRequest(request) self.jar.add_cookie_header(urllib_request) def set(self, name: str, value: str, domain: str = "", path: str = "/") -> None: """ Set a cookie value by name. May optionally include domain and path. """ kwargs = { "version": 0, "name": name, "value": value, "port": None, "port_specified": False, "domain": domain, "domain_specified": bool(domain), "domain_initial_dot": domain.startswith("."), "path": path, "path_specified": bool(path), "secure": False, "expires": None, "discard": True, "comment": None, "comment_url": None, "rest": {"HttpOnly": None}, "rfc2109": False, } cookie = Cookie(**kwargs) # type: ignore self.jar.set_cookie(cookie) def get( # type: ignore self, name: str, default: typing.Optional[str] = None, domain: typing.Optional[str] = None, path: typing.Optional[str] = None, ) -> typing.Optional[str]: """ Get a cookie by name. May optionally include domain and path in order to specify exactly which cookie to retrieve. """ value = None for cookie in self.jar: if cookie.name == name: if domain is None or cookie.domain == domain: if path is None or cookie.path == path: if value is not None: message = f"Multiple cookies exist with name={name}" raise CookieConflict(message) value = cookie.value if value is None: return default return value def delete( self, name: str, domain: typing.Optional[str] = None, path: typing.Optional[str] = None, ) -> None: """ Delete a cookie by name. May optionally include domain and path in order to specify exactly which cookie to delete. """ if domain is not None and path is not None: return self.jar.clear(domain, path, name) remove = [ cookie for cookie in self.jar if cookie.name == name and (domain is None or cookie.domain == domain) and (path is None or cookie.path == path) ] for cookie in remove: self.jar.clear(cookie.domain, cookie.path, cookie.name) def clear( self, domain: typing.Optional[str] = None, path: typing.Optional[str] = None ) -> None: """ Delete all cookies. Optionally include a domain and path in order to only delete a subset of all the cookies. """ args = [] if domain is not None: args.append(domain) if path is not None: assert domain is not None args.append(path) self.jar.clear(*args) def update(self, cookies: typing.Optional[CookieTypes] = None) -> None: # type: ignore cookies = Cookies(cookies) for cookie in cookies.jar: self.jar.set_cookie(cookie) def __setitem__(self, name: str, value: str) -> None: return self.set(name, value) def __getitem__(self, name: str) -> str: value = self.get(name) if value is None: raise KeyError(name) return value def __delitem__(self, name: str) -> None: return self.delete(name) def __len__(self) -> int: return len(self.jar) def __iter__(self) -> typing.Iterator[str]: return (cookie.name for cookie in self.jar) def __bool__(self) -> bool: for _ in self.jar: return True return False def __repr__(self) -> str: cookies_repr = ", ".join( [ f"" for cookie in self.jar ] ) return f"" class _CookieCompatRequest(urllib.request.Request): """ Wraps a `Request` instance up in a compatibility interface suitable for use with `CookieJar` operations. """ def __init__(self, request: Request) -> None: super().__init__( url=str(request.url), headers=dict(request.headers), method=request.method, ) self.request = request def add_unredirected_header(self, key: str, value: str) -> None: super().add_unredirected_header(key, value) self.request.headers[key] = value class _CookieCompatResponse: """ Wraps a `Request` instance up in a compatibility interface suitable for use with `CookieJar` operations. """ def __init__(self, response: Response): self.response = response def info(self) -> email.message.Message: info = email.message.Message() for key, value in self.response.headers.multi_items(): # Note that setting `info[key]` here is an "append" operation, # not a "replace" operation. # https://docs.python.org/3/library/email.compat32-message.html#email.message.Message.__setitem__ info[key] = value return info