# SPDX-License-Identifier: GPLv3-or-later
# Copyright © 2025 pygaindalf Rui Pinheiro
import pathlib
import urllib.parse
from typing import Any, Self
import requests_cache
from ..helpers import script_info
from .config.cache import RequestsCacheBackend
from .config.requests import RequestsConfig
from .filecache import CustomFileCache
from .session import CustomSession
[docs]
class RequestsManager:
_instance = None
[docs]
def __new__(cls, *args, **kwargs) -> Self:
if not cls._instance:
cls._instance = super().__new__(cls, *args, **kwargs)
cls._instance.initialized = False
return cls._instance
[docs]
def __init__(self) -> None:
pass
[docs]
def initialize(self, config: RequestsConfig | dict[str, Any], *, install: bool = False) -> None:
if not isinstance(config, RequestsConfig):
config = RequestsConfig.model_validate(config)
if self.initialized:
msg = f"Must not initialise {type(self).__name__} twice"
raise RuntimeError(msg)
self.initialized = True
self.config = config
if install:
self.install()
def _create_custom_file_cache(self, **kwargs) -> CustomFileCache:
"""Create a custom file cache instance based on the configuration."""
filecache = getattr(self, "filecache", None)
if filecache is None:
filecache = CustomFileCache(**kwargs)
self.filecache = filecache
return filecache
def _get_config_kwargs(self) -> dict[str, Any]:
kwargs = self.config.cache.as_kwargs()
# We use a custom version of FileCache without any SQLite dependencies if the backend is FILESYSTEM
# Since we will be checking in the cache, we want every file to be human-readable
if kwargs["backend"] == RequestsCacheBackend.FILESYSTEM and script_info.is_unit_test():
kwargs["backend"] = self._create_custom_file_cache(**kwargs)
# If the backend is FILESYSTEM, we want to use human-readable cache keys
if kwargs["backend"] == RequestsCacheBackend.FILESYSTEM:
kwargs["key_fn"] = self.human_readable_key_fn
return kwargs
[docs]
def install(self) -> None:
"""Install the requests_cache with the given configuration."""
requests_cache.install_cache(session_factory=CustomSession, **self._get_config_kwargs())
[docs]
def session(self) -> Any:
return CustomSession(**self._get_config_kwargs())
# MARK: Cache methods
[docs]
def human_readable_key_fn(
self,
request: requests_cache.models.AnyRequest,
ignored_parameters: requests_cache.cache_keys.ParamList = None,
match_headers: requests_cache.cache_keys.ParamList | bool = False, # noqa: FBT001, FBT002 as the method signature is defined by the requests library
serializer: Any = None,
**request_kwargs,
) -> str:
request = requests_cache.cache_keys.normalize_request(request, ignored_parameters)
### Parse the URL
url = urllib.parse.urlparse(request.url)
# Domain
domain = str(url.netloc)
assert domain, "Domain must not be empty in the request URL"
# URL Path
urlpath = pathlib.PurePosixPath(urllib.parse.unquote(url.path))
path_parts = [domain, *urlpath.parts[1:]]
assert "/" not in path_parts, "Path parts must not contain a leading slash"
### Create a hash of the request using default implementation of create_key
key_hash = requests_cache.cache_keys.create_key(request, ignored_parameters, match_headers, serializer, **request_kwargs)
### Finish by combining the path and hash
relpath = pathlib.PurePath(*path_parts, key_hash)
assert not relpath.is_absolute(), "Relative path must not be absolute"
abspath = pathlib.PurePath(self.config.cache.cache_name_effective, relpath)
pathlib.Path(abspath.parent).mkdir(exist_ok=True, parents=True)
return str(relpath)