Загрузить файлы в «venv/Lib/site-packages/pip/_vendor/cachecontrol»
This commit is contained in:
32
venv/Lib/site-packages/pip/_vendor/cachecontrol/__init__.py
Normal file
32
venv/Lib/site-packages/pip/_vendor/cachecontrol/__init__.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2015 Eric Larson
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
"""CacheControl import Interface.
|
||||||
|
|
||||||
|
Make it easy to import from cachecontrol without long namespaces.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import importlib.metadata
|
||||||
|
|
||||||
|
from pip._vendor.cachecontrol.adapter import CacheControlAdapter
|
||||||
|
from pip._vendor.cachecontrol.controller import CacheController
|
||||||
|
from pip._vendor.cachecontrol.wrapper import CacheControl
|
||||||
|
|
||||||
|
__author__ = "Eric Larson"
|
||||||
|
__email__ = "eric@ionrock.org"
|
||||||
|
# pip patch: this won't work when vendored, so just patch it out as it's unused
|
||||||
|
# __version__ = importlib.metadata.version("cachecontrol")
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"__author__",
|
||||||
|
"__email__",
|
||||||
|
"__version__",
|
||||||
|
"CacheControlAdapter",
|
||||||
|
"CacheController",
|
||||||
|
"CacheControl",
|
||||||
|
]
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
||||||
70
venv/Lib/site-packages/pip/_vendor/cachecontrol/_cmd.py
Normal file
70
venv/Lib/site-packages/pip/_vendor/cachecontrol/_cmd.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2015 Eric Larson
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from pip._vendor import requests
|
||||||
|
|
||||||
|
from pip._vendor.cachecontrol.adapter import CacheControlAdapter
|
||||||
|
from pip._vendor.cachecontrol.cache import DictCache
|
||||||
|
from pip._vendor.cachecontrol.controller import logger
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from argparse import Namespace
|
||||||
|
|
||||||
|
from pip._vendor.cachecontrol.controller import CacheController
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging() -> None:
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
handler = logging.StreamHandler()
|
||||||
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
|
||||||
|
def get_session() -> requests.Session:
|
||||||
|
adapter = CacheControlAdapter(
|
||||||
|
DictCache(), cache_etags=True, serializer=None, heuristic=None
|
||||||
|
)
|
||||||
|
sess = requests.Session()
|
||||||
|
sess.mount("http://", adapter)
|
||||||
|
sess.mount("https://", adapter)
|
||||||
|
|
||||||
|
sess.cache_controller = adapter.controller # type: ignore[attr-defined]
|
||||||
|
return sess
|
||||||
|
|
||||||
|
|
||||||
|
def get_args() -> Namespace:
|
||||||
|
parser = ArgumentParser()
|
||||||
|
parser.add_argument("url", help="The URL to try and cache")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = get_args()
|
||||||
|
sess = get_session()
|
||||||
|
|
||||||
|
# Make a request to get a response
|
||||||
|
resp = sess.get(args.url)
|
||||||
|
|
||||||
|
# Turn on logging
|
||||||
|
setup_logging()
|
||||||
|
|
||||||
|
# try setting the cache
|
||||||
|
cache_controller: CacheController = (
|
||||||
|
sess.cache_controller # type: ignore[attr-defined]
|
||||||
|
)
|
||||||
|
cache_controller.cache_response(resp.request, resp.raw)
|
||||||
|
|
||||||
|
# Now try to get it
|
||||||
|
if cache_controller.cached_request(resp.request):
|
||||||
|
print("Cached!")
|
||||||
|
else:
|
||||||
|
print("Not cached :(")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
167
venv/Lib/site-packages/pip/_vendor/cachecontrol/adapter.py
Normal file
167
venv/Lib/site-packages/pip/_vendor/cachecontrol/adapter.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2015 Eric Larson
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import weakref
|
||||||
|
import zlib
|
||||||
|
from typing import TYPE_CHECKING, Any, Collection, Mapping
|
||||||
|
|
||||||
|
from pip._vendor.requests.adapters import HTTPAdapter
|
||||||
|
|
||||||
|
from pip._vendor.cachecontrol.cache import DictCache
|
||||||
|
from pip._vendor.cachecontrol.controller import PERMANENT_REDIRECT_STATUSES, CacheController
|
||||||
|
from pip._vendor.cachecontrol.filewrapper import CallbackFileWrapper
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pip._vendor.requests import PreparedRequest, Response
|
||||||
|
from pip._vendor.urllib3 import HTTPResponse
|
||||||
|
|
||||||
|
from pip._vendor.cachecontrol.cache import BaseCache
|
||||||
|
from pip._vendor.cachecontrol.heuristics import BaseHeuristic
|
||||||
|
from pip._vendor.cachecontrol.serialize import Serializer
|
||||||
|
|
||||||
|
|
||||||
|
class CacheControlAdapter(HTTPAdapter):
|
||||||
|
invalidating_methods = {"PUT", "PATCH", "DELETE"}
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
cache: BaseCache | None = None,
|
||||||
|
cache_etags: bool = True,
|
||||||
|
controller_class: type[CacheController] | None = None,
|
||||||
|
serializer: Serializer | None = None,
|
||||||
|
heuristic: BaseHeuristic | None = None,
|
||||||
|
cacheable_methods: Collection[str] | None = None,
|
||||||
|
*args: Any,
|
||||||
|
**kw: Any,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(*args, **kw)
|
||||||
|
self.cache = DictCache() if cache is None else cache
|
||||||
|
self.heuristic = heuristic
|
||||||
|
self.cacheable_methods = cacheable_methods or ("GET",)
|
||||||
|
|
||||||
|
controller_factory = controller_class or CacheController
|
||||||
|
self.controller = controller_factory(
|
||||||
|
self.cache, cache_etags=cache_etags, serializer=serializer
|
||||||
|
)
|
||||||
|
|
||||||
|
def send(
|
||||||
|
self,
|
||||||
|
request: PreparedRequest,
|
||||||
|
stream: bool = False,
|
||||||
|
timeout: None | float | tuple[float, float] | tuple[float, None] = None,
|
||||||
|
verify: bool | str = True,
|
||||||
|
cert: (None | bytes | str | tuple[bytes | str, bytes | str]) = None,
|
||||||
|
proxies: Mapping[str, str] | None = None,
|
||||||
|
cacheable_methods: Collection[str] | None = None,
|
||||||
|
) -> Response:
|
||||||
|
"""
|
||||||
|
Send a request. Use the request information to see if it
|
||||||
|
exists in the cache and cache the response if we need to and can.
|
||||||
|
"""
|
||||||
|
cacheable = cacheable_methods or self.cacheable_methods
|
||||||
|
if request.method in cacheable:
|
||||||
|
try:
|
||||||
|
cached_response = self.controller.cached_request(request)
|
||||||
|
except zlib.error:
|
||||||
|
cached_response = None
|
||||||
|
if cached_response:
|
||||||
|
return self.build_response(request, cached_response, from_cache=True)
|
||||||
|
|
||||||
|
# check for etags and add headers if appropriate
|
||||||
|
request.headers.update(self.controller.conditional_headers(request))
|
||||||
|
|
||||||
|
resp = super().send(request, stream, timeout, verify, cert, proxies)
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def build_response( # type: ignore[override]
|
||||||
|
self,
|
||||||
|
request: PreparedRequest,
|
||||||
|
response: HTTPResponse,
|
||||||
|
from_cache: bool = False,
|
||||||
|
cacheable_methods: Collection[str] | None = None,
|
||||||
|
) -> Response:
|
||||||
|
"""
|
||||||
|
Build a response by making a request or using the cache.
|
||||||
|
|
||||||
|
This will end up calling send and returning a potentially
|
||||||
|
cached response
|
||||||
|
"""
|
||||||
|
cacheable = cacheable_methods or self.cacheable_methods
|
||||||
|
if not from_cache and request.method in cacheable:
|
||||||
|
# Check for any heuristics that might update headers
|
||||||
|
# before trying to cache.
|
||||||
|
if self.heuristic:
|
||||||
|
response = self.heuristic.apply(response)
|
||||||
|
|
||||||
|
# apply any expiration heuristics
|
||||||
|
if response.status == 304:
|
||||||
|
# We must have sent an ETag request. This could mean
|
||||||
|
# that we've been expired already or that we simply
|
||||||
|
# have an etag. In either case, we want to try and
|
||||||
|
# update the cache if that is the case.
|
||||||
|
cached_response = self.controller.update_cached_response(
|
||||||
|
request, response
|
||||||
|
)
|
||||||
|
|
||||||
|
if cached_response is not response:
|
||||||
|
from_cache = True
|
||||||
|
|
||||||
|
# We are done with the server response, read a
|
||||||
|
# possible response body (compliant servers will
|
||||||
|
# not return one, but we cannot be 100% sure) and
|
||||||
|
# release the connection back to the pool.
|
||||||
|
response.read(decode_content=False)
|
||||||
|
response.release_conn()
|
||||||
|
|
||||||
|
response = cached_response
|
||||||
|
|
||||||
|
# We always cache the 301 responses
|
||||||
|
elif int(response.status) in PERMANENT_REDIRECT_STATUSES:
|
||||||
|
self.controller.cache_response(request, response)
|
||||||
|
else:
|
||||||
|
# Wrap the response file with a wrapper that will cache the
|
||||||
|
# response when the stream has been consumed.
|
||||||
|
response._fp = CallbackFileWrapper( # type: ignore[assignment]
|
||||||
|
response._fp, # type: ignore[arg-type]
|
||||||
|
functools.partial(
|
||||||
|
self.controller.cache_response, request, weakref.ref(response)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if response.chunked:
|
||||||
|
super_update_chunk_length = response.__class__._update_chunk_length
|
||||||
|
|
||||||
|
def _update_chunk_length(
|
||||||
|
weak_self: weakref.ReferenceType[HTTPResponse],
|
||||||
|
) -> None:
|
||||||
|
self = weak_self()
|
||||||
|
if self is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
super_update_chunk_length(self)
|
||||||
|
if self.chunk_left == 0:
|
||||||
|
self._fp._close() # type: ignore[union-attr]
|
||||||
|
|
||||||
|
response._update_chunk_length = functools.partial( # type: ignore[method-assign]
|
||||||
|
_update_chunk_length, weakref.ref(response)
|
||||||
|
)
|
||||||
|
|
||||||
|
resp: Response = super().build_response(request, response)
|
||||||
|
|
||||||
|
# See if we should invalidate the cache.
|
||||||
|
if request.method in self.invalidating_methods and resp.ok:
|
||||||
|
assert request.url is not None
|
||||||
|
cache_url = self.controller.cache_url(request.url)
|
||||||
|
self.cache.delete(cache_url)
|
||||||
|
|
||||||
|
# Give the request a from_cache attr to let people use it
|
||||||
|
resp.from_cache = from_cache # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
self.cache.close()
|
||||||
|
super().close() # type: ignore[no-untyped-call]
|
||||||
75
venv/Lib/site-packages/pip/_vendor/cachecontrol/cache.py
Normal file
75
venv/Lib/site-packages/pip/_vendor/cachecontrol/cache.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2015 Eric Larson
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
The cache object API for implementing caches. The default is a thread
|
||||||
|
safe in-memory dictionary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from threading import Lock
|
||||||
|
from typing import IO, TYPE_CHECKING, MutableMapping
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class BaseCache:
|
||||||
|
def get(self, key: str) -> bytes | None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def set(
|
||||||
|
self, key: str, value: bytes, expires: int | datetime | None = None
|
||||||
|
) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def delete(self, key: str) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DictCache(BaseCache):
|
||||||
|
def __init__(self, init_dict: MutableMapping[str, bytes] | None = None) -> None:
|
||||||
|
self.lock = Lock()
|
||||||
|
self.data = init_dict or {}
|
||||||
|
|
||||||
|
def get(self, key: str) -> bytes | None:
|
||||||
|
return self.data.get(key, None)
|
||||||
|
|
||||||
|
def set(
|
||||||
|
self, key: str, value: bytes, expires: int | datetime | None = None
|
||||||
|
) -> None:
|
||||||
|
with self.lock:
|
||||||
|
self.data.update({key: value})
|
||||||
|
|
||||||
|
def delete(self, key: str) -> None:
|
||||||
|
with self.lock:
|
||||||
|
if key in self.data:
|
||||||
|
self.data.pop(key)
|
||||||
|
|
||||||
|
|
||||||
|
class SeparateBodyBaseCache(BaseCache):
|
||||||
|
"""
|
||||||
|
In this variant, the body is not stored mixed in with the metadata, but is
|
||||||
|
passed in (as a bytes-like object) in a separate call to ``set_body()``.
|
||||||
|
|
||||||
|
That is, the expected interaction pattern is::
|
||||||
|
|
||||||
|
cache.set(key, serialized_metadata)
|
||||||
|
cache.set_body(key)
|
||||||
|
|
||||||
|
Similarly, the body should be loaded separately via ``get_body()``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def set_body(self, key: str, body: bytes) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def get_body(self, key: str) -> IO[bytes] | None:
|
||||||
|
"""
|
||||||
|
Return the body as file-like object.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
511
venv/Lib/site-packages/pip/_vendor/cachecontrol/controller.py
Normal file
511
venv/Lib/site-packages/pip/_vendor/cachecontrol/controller.py
Normal file
@@ -0,0 +1,511 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2015 Eric Larson
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
The httplib2 algorithms ported for use with requests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import calendar
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import weakref
|
||||||
|
from email.utils import parsedate_tz
|
||||||
|
from typing import TYPE_CHECKING, Collection, Mapping
|
||||||
|
|
||||||
|
from pip._vendor.requests.structures import CaseInsensitiveDict
|
||||||
|
|
||||||
|
from pip._vendor.cachecontrol.cache import DictCache, SeparateBodyBaseCache
|
||||||
|
from pip._vendor.cachecontrol.serialize import Serializer
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pip._vendor.requests import PreparedRequest
|
||||||
|
from pip._vendor.urllib3 import HTTPResponse
|
||||||
|
|
||||||
|
from pip._vendor.cachecontrol.cache import BaseCache
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
URI = re.compile(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?")
|
||||||
|
|
||||||
|
PERMANENT_REDIRECT_STATUSES = (301, 308)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_uri(uri: str) -> tuple[str, str, str, str, str]:
|
||||||
|
"""Parses a URI using the regex given in Appendix B of RFC 3986.
|
||||||
|
|
||||||
|
(scheme, authority, path, query, fragment) = parse_uri(uri)
|
||||||
|
"""
|
||||||
|
match = URI.match(uri)
|
||||||
|
assert match is not None
|
||||||
|
groups = match.groups()
|
||||||
|
return (groups[1], groups[3], groups[4], groups[6], groups[8])
|
||||||
|
|
||||||
|
|
||||||
|
class CacheController:
|
||||||
|
"""An interface to see if request should cached or not."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
cache: BaseCache | None = None,
|
||||||
|
cache_etags: bool = True,
|
||||||
|
serializer: Serializer | None = None,
|
||||||
|
status_codes: Collection[int] | None = None,
|
||||||
|
):
|
||||||
|
self.cache = DictCache() if cache is None else cache
|
||||||
|
self.cache_etags = cache_etags
|
||||||
|
self.serializer = serializer or Serializer()
|
||||||
|
self.cacheable_status_codes = status_codes or (200, 203, 300, 301, 308)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _urlnorm(cls, uri: str) -> str:
|
||||||
|
"""Normalize the URL to create a safe key for the cache"""
|
||||||
|
(scheme, authority, path, query, fragment) = parse_uri(uri)
|
||||||
|
if not scheme or not authority:
|
||||||
|
raise Exception("Only absolute URIs are allowed. uri = %s" % uri)
|
||||||
|
|
||||||
|
scheme = scheme.lower()
|
||||||
|
authority = authority.lower()
|
||||||
|
|
||||||
|
if not path:
|
||||||
|
path = "/"
|
||||||
|
|
||||||
|
# Could do syntax based normalization of the URI before
|
||||||
|
# computing the digest. See Section 6.2.2 of Std 66.
|
||||||
|
request_uri = query and "?".join([path, query]) or path
|
||||||
|
defrag_uri = scheme + "://" + authority + request_uri
|
||||||
|
|
||||||
|
return defrag_uri
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def cache_url(cls, uri: str) -> str:
|
||||||
|
return cls._urlnorm(uri)
|
||||||
|
|
||||||
|
def parse_cache_control(self, headers: Mapping[str, str]) -> dict[str, int | None]:
|
||||||
|
known_directives = {
|
||||||
|
# https://tools.ietf.org/html/rfc7234#section-5.2
|
||||||
|
"max-age": (int, True),
|
||||||
|
"max-stale": (int, False),
|
||||||
|
"min-fresh": (int, True),
|
||||||
|
"no-cache": (None, False),
|
||||||
|
"no-store": (None, False),
|
||||||
|
"no-transform": (None, False),
|
||||||
|
"only-if-cached": (None, False),
|
||||||
|
"must-revalidate": (None, False),
|
||||||
|
"public": (None, False),
|
||||||
|
"private": (None, False),
|
||||||
|
"proxy-revalidate": (None, False),
|
||||||
|
"s-maxage": (int, True),
|
||||||
|
}
|
||||||
|
|
||||||
|
cc_headers = headers.get("cache-control", headers.get("Cache-Control", ""))
|
||||||
|
|
||||||
|
retval: dict[str, int | None] = {}
|
||||||
|
|
||||||
|
for cc_directive in cc_headers.split(","):
|
||||||
|
if not cc_directive.strip():
|
||||||
|
continue
|
||||||
|
|
||||||
|
parts = cc_directive.split("=", 1)
|
||||||
|
directive = parts[0].strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
typ, required = known_directives[directive]
|
||||||
|
except KeyError:
|
||||||
|
logger.debug("Ignoring unknown cache-control directive: %s", directive)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not typ or not required:
|
||||||
|
retval[directive] = None
|
||||||
|
if typ:
|
||||||
|
try:
|
||||||
|
retval[directive] = typ(parts[1].strip())
|
||||||
|
except IndexError:
|
||||||
|
if required:
|
||||||
|
logger.debug(
|
||||||
|
"Missing value for cache-control " "directive: %s",
|
||||||
|
directive,
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
logger.debug(
|
||||||
|
"Invalid value for cache-control directive " "%s, must be %s",
|
||||||
|
directive,
|
||||||
|
typ.__name__,
|
||||||
|
)
|
||||||
|
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def _load_from_cache(self, request: PreparedRequest) -> HTTPResponse | None:
|
||||||
|
"""
|
||||||
|
Load a cached response, or return None if it's not available.
|
||||||
|
"""
|
||||||
|
# We do not support caching of partial content: so if the request contains a
|
||||||
|
# Range header then we don't want to load anything from the cache.
|
||||||
|
if "Range" in request.headers:
|
||||||
|
return None
|
||||||
|
|
||||||
|
cache_url = request.url
|
||||||
|
assert cache_url is not None
|
||||||
|
cache_data = self.cache.get(cache_url)
|
||||||
|
if cache_data is None:
|
||||||
|
logger.debug("No cache entry available")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(self.cache, SeparateBodyBaseCache):
|
||||||
|
body_file = self.cache.get_body(cache_url)
|
||||||
|
else:
|
||||||
|
body_file = None
|
||||||
|
|
||||||
|
result = self.serializer.loads(request, cache_data, body_file)
|
||||||
|
if result is None:
|
||||||
|
logger.warning("Cache entry deserialization failed, entry ignored")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def cached_request(self, request: PreparedRequest) -> HTTPResponse | Literal[False]:
|
||||||
|
"""
|
||||||
|
Return a cached response if it exists in the cache, otherwise
|
||||||
|
return False.
|
||||||
|
"""
|
||||||
|
assert request.url is not None
|
||||||
|
cache_url = self.cache_url(request.url)
|
||||||
|
logger.debug('Looking up "%s" in the cache', cache_url)
|
||||||
|
cc = self.parse_cache_control(request.headers)
|
||||||
|
|
||||||
|
# Bail out if the request insists on fresh data
|
||||||
|
if "no-cache" in cc:
|
||||||
|
logger.debug('Request header has "no-cache", cache bypassed')
|
||||||
|
return False
|
||||||
|
|
||||||
|
if "max-age" in cc and cc["max-age"] == 0:
|
||||||
|
logger.debug('Request header has "max_age" as 0, cache bypassed')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check whether we can load the response from the cache:
|
||||||
|
resp = self._load_from_cache(request)
|
||||||
|
if not resp:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# If we have a cached permanent redirect, return it immediately. We
|
||||||
|
# don't need to test our response for other headers b/c it is
|
||||||
|
# intrinsically "cacheable" as it is Permanent.
|
||||||
|
#
|
||||||
|
# See:
|
||||||
|
# https://tools.ietf.org/html/rfc7231#section-6.4.2
|
||||||
|
#
|
||||||
|
# Client can try to refresh the value by repeating the request
|
||||||
|
# with cache busting headers as usual (ie no-cache).
|
||||||
|
if int(resp.status) in PERMANENT_REDIRECT_STATUSES:
|
||||||
|
msg = (
|
||||||
|
"Returning cached permanent redirect response "
|
||||||
|
"(ignoring date and etag information)"
|
||||||
|
)
|
||||||
|
logger.debug(msg)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
headers: CaseInsensitiveDict[str] = CaseInsensitiveDict(resp.headers)
|
||||||
|
if not headers or "date" not in headers:
|
||||||
|
if "etag" not in headers:
|
||||||
|
# Without date or etag, the cached response can never be used
|
||||||
|
# and should be deleted.
|
||||||
|
logger.debug("Purging cached response: no date or etag")
|
||||||
|
self.cache.delete(cache_url)
|
||||||
|
logger.debug("Ignoring cached response: no date")
|
||||||
|
return False
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
time_tuple = parsedate_tz(headers["date"])
|
||||||
|
assert time_tuple is not None
|
||||||
|
date = calendar.timegm(time_tuple[:6])
|
||||||
|
current_age = max(0, now - date)
|
||||||
|
logger.debug("Current age based on date: %i", current_age)
|
||||||
|
|
||||||
|
# TODO: There is an assumption that the result will be a
|
||||||
|
# urllib3 response object. This may not be best since we
|
||||||
|
# could probably avoid instantiating or constructing the
|
||||||
|
# response until we know we need it.
|
||||||
|
resp_cc = self.parse_cache_control(headers)
|
||||||
|
|
||||||
|
# determine freshness
|
||||||
|
freshness_lifetime = 0
|
||||||
|
|
||||||
|
# Check the max-age pragma in the cache control header
|
||||||
|
max_age = resp_cc.get("max-age")
|
||||||
|
if max_age is not None:
|
||||||
|
freshness_lifetime = max_age
|
||||||
|
logger.debug("Freshness lifetime from max-age: %i", freshness_lifetime)
|
||||||
|
|
||||||
|
# If there isn't a max-age, check for an expires header
|
||||||
|
elif "expires" in headers:
|
||||||
|
expires = parsedate_tz(headers["expires"])
|
||||||
|
if expires is not None:
|
||||||
|
expire_time = calendar.timegm(expires[:6]) - date
|
||||||
|
freshness_lifetime = max(0, expire_time)
|
||||||
|
logger.debug("Freshness lifetime from expires: %i", freshness_lifetime)
|
||||||
|
|
||||||
|
# Determine if we are setting freshness limit in the
|
||||||
|
# request. Note, this overrides what was in the response.
|
||||||
|
max_age = cc.get("max-age")
|
||||||
|
if max_age is not None:
|
||||||
|
freshness_lifetime = max_age
|
||||||
|
logger.debug(
|
||||||
|
"Freshness lifetime from request max-age: %i", freshness_lifetime
|
||||||
|
)
|
||||||
|
|
||||||
|
min_fresh = cc.get("min-fresh")
|
||||||
|
if min_fresh is not None:
|
||||||
|
# adjust our current age by our min fresh
|
||||||
|
current_age += min_fresh
|
||||||
|
logger.debug("Adjusted current age from min-fresh: %i", current_age)
|
||||||
|
|
||||||
|
# Return entry if it is fresh enough
|
||||||
|
if freshness_lifetime > current_age:
|
||||||
|
logger.debug('The response is "fresh", returning cached response')
|
||||||
|
logger.debug("%i > %i", freshness_lifetime, current_age)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
# we're not fresh. If we don't have an Etag, clear it out
|
||||||
|
if "etag" not in headers:
|
||||||
|
logger.debug('The cached response is "stale" with no etag, purging')
|
||||||
|
self.cache.delete(cache_url)
|
||||||
|
|
||||||
|
# return the original handler
|
||||||
|
return False
|
||||||
|
|
||||||
|
def conditional_headers(self, request: PreparedRequest) -> dict[str, str]:
|
||||||
|
resp = self._load_from_cache(request)
|
||||||
|
new_headers = {}
|
||||||
|
|
||||||
|
if resp:
|
||||||
|
headers: CaseInsensitiveDict[str] = CaseInsensitiveDict(resp.headers)
|
||||||
|
|
||||||
|
if "etag" in headers:
|
||||||
|
new_headers["If-None-Match"] = headers["ETag"]
|
||||||
|
|
||||||
|
if "last-modified" in headers:
|
||||||
|
new_headers["If-Modified-Since"] = headers["Last-Modified"]
|
||||||
|
|
||||||
|
return new_headers
|
||||||
|
|
||||||
|
def _cache_set(
|
||||||
|
self,
|
||||||
|
cache_url: str,
|
||||||
|
request: PreparedRequest,
|
||||||
|
response: HTTPResponse,
|
||||||
|
body: bytes | None = None,
|
||||||
|
expires_time: int | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Store the data in the cache.
|
||||||
|
"""
|
||||||
|
if isinstance(self.cache, SeparateBodyBaseCache):
|
||||||
|
# We pass in the body separately; just put a placeholder empty
|
||||||
|
# string in the metadata.
|
||||||
|
self.cache.set(
|
||||||
|
cache_url,
|
||||||
|
self.serializer.dumps(request, response, b""),
|
||||||
|
expires=expires_time,
|
||||||
|
)
|
||||||
|
# body is None can happen when, for example, we're only updating
|
||||||
|
# headers, as is the case in update_cached_response().
|
||||||
|
if body is not None:
|
||||||
|
self.cache.set_body(cache_url, body)
|
||||||
|
else:
|
||||||
|
self.cache.set(
|
||||||
|
cache_url,
|
||||||
|
self.serializer.dumps(request, response, body),
|
||||||
|
expires=expires_time,
|
||||||
|
)
|
||||||
|
|
||||||
|
def cache_response(
|
||||||
|
self,
|
||||||
|
request: PreparedRequest,
|
||||||
|
response_or_ref: HTTPResponse | weakref.ReferenceType[HTTPResponse],
|
||||||
|
body: bytes | None = None,
|
||||||
|
status_codes: Collection[int] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Algorithm for caching requests.
|
||||||
|
|
||||||
|
This assumes a requests Response object.
|
||||||
|
"""
|
||||||
|
if isinstance(response_or_ref, weakref.ReferenceType):
|
||||||
|
response = response_or_ref()
|
||||||
|
if response is None:
|
||||||
|
# The weakref can be None only in case the user used streamed request
|
||||||
|
# and did not consume or close it, and holds no reference to requests.Response.
|
||||||
|
# In such case, we don't want to cache the response.
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
response = response_or_ref
|
||||||
|
|
||||||
|
# From httplib2: Don't cache 206's since we aren't going to
|
||||||
|
# handle byte range requests
|
||||||
|
cacheable_status_codes = status_codes or self.cacheable_status_codes
|
||||||
|
if response.status not in cacheable_status_codes:
|
||||||
|
logger.debug(
|
||||||
|
"Status code %s not in %s", response.status, cacheable_status_codes
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
response_headers: CaseInsensitiveDict[str] = CaseInsensitiveDict(
|
||||||
|
response.headers
|
||||||
|
)
|
||||||
|
|
||||||
|
if "date" in response_headers:
|
||||||
|
time_tuple = parsedate_tz(response_headers["date"])
|
||||||
|
assert time_tuple is not None
|
||||||
|
date = calendar.timegm(time_tuple[:6])
|
||||||
|
else:
|
||||||
|
date = 0
|
||||||
|
|
||||||
|
# If we've been given a body, our response has a Content-Length, that
|
||||||
|
# Content-Length is valid then we can check to see if the body we've
|
||||||
|
# been given matches the expected size, and if it doesn't we'll just
|
||||||
|
# skip trying to cache it.
|
||||||
|
if (
|
||||||
|
body is not None
|
||||||
|
and "content-length" in response_headers
|
||||||
|
and response_headers["content-length"].isdigit()
|
||||||
|
and int(response_headers["content-length"]) != len(body)
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
cc_req = self.parse_cache_control(request.headers)
|
||||||
|
cc = self.parse_cache_control(response_headers)
|
||||||
|
|
||||||
|
assert request.url is not None
|
||||||
|
cache_url = self.cache_url(request.url)
|
||||||
|
logger.debug('Updating cache with response from "%s"', cache_url)
|
||||||
|
|
||||||
|
# Delete it from the cache if we happen to have it stored there
|
||||||
|
no_store = False
|
||||||
|
if "no-store" in cc:
|
||||||
|
no_store = True
|
||||||
|
logger.debug('Response header has "no-store"')
|
||||||
|
if "no-store" in cc_req:
|
||||||
|
no_store = True
|
||||||
|
logger.debug('Request header has "no-store"')
|
||||||
|
if no_store and self.cache.get(cache_url):
|
||||||
|
logger.debug('Purging existing cache entry to honor "no-store"')
|
||||||
|
self.cache.delete(cache_url)
|
||||||
|
if no_store:
|
||||||
|
return
|
||||||
|
|
||||||
|
# https://tools.ietf.org/html/rfc7234#section-4.1:
|
||||||
|
# A Vary header field-value of "*" always fails to match.
|
||||||
|
# Storing such a response leads to a deserialization warning
|
||||||
|
# during cache lookup and is not allowed to ever be served,
|
||||||
|
# so storing it can be avoided.
|
||||||
|
if "*" in response_headers.get("vary", ""):
|
||||||
|
logger.debug('Response header has "Vary: *"')
|
||||||
|
return
|
||||||
|
|
||||||
|
# If we've been given an etag, then keep the response
|
||||||
|
if self.cache_etags and "etag" in response_headers:
|
||||||
|
expires_time = 0
|
||||||
|
if response_headers.get("expires"):
|
||||||
|
expires = parsedate_tz(response_headers["expires"])
|
||||||
|
if expires is not None:
|
||||||
|
expires_time = calendar.timegm(expires[:6]) - date
|
||||||
|
|
||||||
|
expires_time = max(expires_time, 14 * 86400)
|
||||||
|
|
||||||
|
logger.debug(f"etag object cached for {expires_time} seconds")
|
||||||
|
logger.debug("Caching due to etag")
|
||||||
|
self._cache_set(cache_url, request, response, body, expires_time)
|
||||||
|
|
||||||
|
# Add to the cache any permanent redirects. We do this before looking
|
||||||
|
# that the Date headers.
|
||||||
|
elif int(response.status) in PERMANENT_REDIRECT_STATUSES:
|
||||||
|
logger.debug("Caching permanent redirect")
|
||||||
|
self._cache_set(cache_url, request, response, b"")
|
||||||
|
|
||||||
|
# Add to the cache if the response headers demand it. If there
|
||||||
|
# is no date header then we can't do anything about expiring
|
||||||
|
# the cache.
|
||||||
|
elif "date" in response_headers:
|
||||||
|
time_tuple = parsedate_tz(response_headers["date"])
|
||||||
|
assert time_tuple is not None
|
||||||
|
date = calendar.timegm(time_tuple[:6])
|
||||||
|
# cache when there is a max-age > 0
|
||||||
|
max_age = cc.get("max-age")
|
||||||
|
if max_age is not None and max_age > 0:
|
||||||
|
logger.debug("Caching b/c date exists and max-age > 0")
|
||||||
|
expires_time = max_age
|
||||||
|
self._cache_set(
|
||||||
|
cache_url,
|
||||||
|
request,
|
||||||
|
response,
|
||||||
|
body,
|
||||||
|
expires_time,
|
||||||
|
)
|
||||||
|
|
||||||
|
# If the request can expire, it means we should cache it
|
||||||
|
# in the meantime.
|
||||||
|
elif "expires" in response_headers:
|
||||||
|
if response_headers["expires"]:
|
||||||
|
expires = parsedate_tz(response_headers["expires"])
|
||||||
|
if expires is not None:
|
||||||
|
expires_time = calendar.timegm(expires[:6]) - date
|
||||||
|
else:
|
||||||
|
expires_time = None
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"Caching b/c of expires header. expires in {} seconds".format(
|
||||||
|
expires_time
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._cache_set(
|
||||||
|
cache_url,
|
||||||
|
request,
|
||||||
|
response,
|
||||||
|
body,
|
||||||
|
expires_time,
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_cached_response(
|
||||||
|
self, request: PreparedRequest, response: HTTPResponse
|
||||||
|
) -> HTTPResponse:
|
||||||
|
"""On a 304 we will get a new set of headers that we want to
|
||||||
|
update our cached value with, assuming we have one.
|
||||||
|
|
||||||
|
This should only ever be called when we've sent an ETag and
|
||||||
|
gotten a 304 as the response.
|
||||||
|
"""
|
||||||
|
assert request.url is not None
|
||||||
|
cache_url = self.cache_url(request.url)
|
||||||
|
cached_response = self._load_from_cache(request)
|
||||||
|
|
||||||
|
if not cached_response:
|
||||||
|
# we didn't have a cached response
|
||||||
|
return response
|
||||||
|
|
||||||
|
# Lets update our headers with the headers from the new request:
|
||||||
|
# http://tools.ietf.org/html/draft-ietf-httpbis-p4-conditional-26#section-4.1
|
||||||
|
#
|
||||||
|
# The server isn't supposed to send headers that would make
|
||||||
|
# the cached body invalid. But... just in case, we'll be sure
|
||||||
|
# to strip out ones we know that might be problematic due to
|
||||||
|
# typical assumptions.
|
||||||
|
excluded_headers = ["content-length"]
|
||||||
|
|
||||||
|
cached_response.headers.update(
|
||||||
|
{
|
||||||
|
k: v
|
||||||
|
for k, v in response.headers.items()
|
||||||
|
if k.lower() not in excluded_headers
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# we want a 200 b/c we have content via the cache
|
||||||
|
cached_response.status = 200
|
||||||
|
|
||||||
|
# update our cache
|
||||||
|
self._cache_set(cache_url, request, cached_response)
|
||||||
|
|
||||||
|
return cached_response
|
||||||
Reference in New Issue
Block a user