262 行
8.3 KiB
Python
262 行
8.3 KiB
Python
# 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"<Cookie {cookie.name}={cookie.value} for {cookie.domain} />"
|
|
for cookie in self.jar
|
|
]
|
|
)
|
|
|
|
return f"<Cookies[{cookies_repr}]>"
|
|
|
|
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
|